// EL PURO - SilkNet OS Core Engine (Authoritative Server Synchronization) const INITIAL_GAME_STATE = { cash: 25.0, cleanBonds: 0, totalCleanBonds: 0, heat: 0.0, totalEarned: 0.0, activeTab: 'lab', buyMode: '1', stats: { totalProduced: 0.0, totalSold: 0.0, totalSupplied: 0.0, purityProcs: 0, raidsSurvived: 0, prestigeCount: 0 }, drugs: {}, skills: {}, achievements: {}, // { [achId]: claimed_tier_int } offshorePerks: {}, // { [perkId]: level_int } 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; }); ACHIEVEMENT_TIERS.forEach(a => { INITIAL_GAME_STATE.achievements[a.id] = 0; }); OFFSHORE_PERKS.forEach(p => { INITIAL_GAME_STATE.offshorePerks[p.id] = 0; }); 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 waiting to be debounced & flushed let inFlightActions = []; // Actions currently travelling in an active HTTP request let actionSeq = 0; // Monotonic sequence ID let syncDebounceTimer = null; // 300ms debounce timer for rapid clicking let isSyncing = false; // Network in-flight flag // --- 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; 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'), quickStash: document.getElementById('quick-stash-items'), trophyBadge: document.getElementById('trophy-badge') }; // --- MILESTONES & BULK FORMULAS --- function getUnitMilestoneMultiplier(count) { const reached = UNIT_MILESTONES.filter(m => count >= m).length; return Math.pow(2, reached); } function getNextMilestone(count) { const next = UNIT_MILESTONES.find(m => m > count); return next || (Math.floor(count / 1000) + 1) * 1000; } function getAllTierMilestoneMultiplier(unitKey) { const unlocked = DRUG_CATALOG.filter(d => G.drugs[d.id] && G.drugs[d.id].unlocked); if (!unlocked.length) return 1.0; const minCount = Math.min(...unlocked.map(d => (G.drugs[d.id] && G.drugs[d.id][unitKey]) || 0)); const reached = UNIT_MILESTONES.filter(m => minCount >= m).length; return Math.pow(2, reached); } function getPriceScaling() { const discountLvl = (G.offshorePerks && G.offshorePerks['bulk_discount']) || 0; return Math.max(1.10, 1.15 - (discountLvl * 0.01)); } // Geometric Series Cost: sum of baseCost * r^(count + i) for i = 0 to n-1 function getGeometricBulkCost(baseCost, currentCount, n) { if (n <= 0) return 0; const r = getPriceScaling(); const firstTerm = baseCost * Math.pow(r, currentCount); if (n === 1) return Math.floor(firstTerm); const total = firstTerm * (Math.pow(r, n) - 1.0) / (r - 1.0); return Math.floor(total); } // Maximum units affordable with available cash function getMaxAffordableUnits(baseCost, currentCount, availableCash) { if (availableCash <= 0) return 0; const r = getPriceScaling(); const firstTerm = baseCost * Math.pow(r, currentCount); if (availableCash < firstTerm) return 0; const numerator = availableCash * (r - 1.0); const n = Math.floor(Math.log(1.0 + (numerator / firstTerm)) / Math.log(r)); return Math.max(0, n); } // Helper to determine exact unit count & total price based on active G.buyMode function getUnitPurchaseInfo(drugId, unitType) { const config = DRUG_CATALOG.find(d => d.id === drugId); const data = G.drugs[drugId] || { suppliers: 0, workers: 0, dealers: 0 }; const currentCount = data[unitType] || 0; const baseCost = unitType === 'suppliers' ? (config.supplierCost || 75) : unitType === 'workers' ? config.workerCost : config.dealerCost; let qty = 1; if (G.buyMode === '10') { qty = 10; } else if (G.buyMode === '100') { qty = 100; } else if (G.buyMode === 'next') { const nextM = getNextMilestone(currentCount); qty = Math.max(1, nextM - currentCount); } else if (G.buyMode === 'max') { const maxQty = getMaxAffordableUnits(baseCost, currentCount, G.cash); qty = Math.max(1, maxQty); } const totalCost = getGeometricBulkCost(baseCost, currentCount, qty); return { qty, totalCost, baseCost, currentCount }; } function getRawPurchaseInfo(drugId) { const d = DRUG_CATALOG.find(x => x.id === drugId); const unitCost = Math.floor(d.rawCost * getRawCostMultiplier()); let qty = 1; if (G.buyMode === '10') qty = 10; else if (G.buyMode === '100') qty = 100; else if (G.buyMode === 'next') qty = 25; else if (G.buyMode === 'max') qty = Math.max(1, Math.floor(G.cash / unitCost)); const totalCost = unitCost * qty; return { qty, totalCost }; } // --- MULTIPLIERS & BONUSES --- function getProductionMultiplier() { let m = 1.0; const totalBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0); m += totalBonds * 0.03; if (G.skills['chem_1']) m += 0.15; // Tiered achievements const synthTier = (G.achievements && G.achievements['master_synthesizer']) || 0; m += [0, 0.05, 0.10, 0.15, 0.25, 0.50][Math.min(5, synthTier)]; const formTier = (G.achievements && G.achievements['formula_expansion']) || 0; m += [0, 0.05, 0.10, 0.15, 0.30][Math.min(4, formTier)]; return m; } function getSellPriceMultiplier() { let m = 1.0; const totalBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0); const compoundLvl = (G.offshorePerks && G.offshorePerks['compound_interest']) || 0; const bondRate = 0.04 + (compoundLvl * 0.02); m += totalBonds * bondRate; if (G.skills['king_2']) m += 0.25; const empireTier = (G.achievements && G.achievements['dirty_empire']) || 0; m += [0, 0.05, 0.10, 0.15, 0.25, 0.50][Math.min(5, empireTier)]; return m; } function getRawCostMultiplier() { let m = 1.0; if (G.skills['chem_2']) m *= 0.85; const vaultTier = (G.achievements && G.achievements['vault_hoarder']) || 0; const discount = [0, 0.05, 0.10, 0.15, 0.25][Math.min(4, vaultTier)]; return Math.max(0.1, m - discount); } function getDealerSpeedMultiplier() { let m = 1.0; if (G.skills['king_1']) m += 0.20; if (G.skills['king_3']) m += 0.50; const streetTier = (G.achievements && G.achievements['street_monopoly']) || 0; m += [0, 0.05, 0.10, 0.15, 0.25, 0.50][Math.min(5, streetTier)]; const armyTier = (G.achievements && G.achievements['syndicate_army']) || 0; m *= (1.0 + [0, 0.05, 0.10, 0.15, 0.25][Math.min(4, armyTier)]); return m; } function getSupplierSpeedMultiplier() { let m = 1.0; const supplyTier = (G.achievements && G.achievements['supply_baron']) || 0; m += [0, 0.05, 0.10, 0.15, 0.25][Math.min(4, supplyTier)]; const armyTier = (G.achievements && G.achievements['syndicate_army']) || 0; m *= (1.0 + [0, 0.05, 0.10, 0.15, 0.25][Math.min(4, armyTier)]); return m; } function getWorkerSpeedMultiplier() { let m = 1.0; const armyTier = (G.achievements && G.achievements['syndicate_army']) || 0; m *= (1.0 + [0, 0.05, 0.10, 0.15, 0.25][Math.min(4, armyTier)]); return m; } 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 === 'trophies') { renderTrophiesView(); } 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 }; if (!data.unlocked) { const unlockCost = getUnlockCost(d.id); html += `
${d.name}
CLASSIFIED FORMULA
`; return; } const suppliers = data.suppliers || 0; const workers = data.workers || 0; const supplyRate = d.supplyRate || 1.0; const supMilestoneM = getUnitMilestoneMultiplier(suppliers); const workMilestoneM = getUnitMilestoneMultiplier(workers); const allTierSupM = getAllTierMilestoneMultiplier('suppliers'); const allTierWorkM = getAllTierMilestoneMultiplier('workers'); const autoSupplySpeed = suppliers * supplyRate * getProductionMultiplier() * getSupplierSpeedMultiplier() * supMilestoneM * allTierSupM; const autoProdSpeed = workers * d.produceRate * getProductionMultiplier() * getWorkerSpeedMultiplier() * workMilestoneM * allTierWorkM; const nextSupM = getNextMilestone(suppliers); const nextWorkM = getNextMilestone(workers); const rawInfo = getRawPurchaseInfo(d.id); const supInfo = getUnitPurchaseInfo(d.id, 'suppliers'); const workInfo = getUnitPurchaseInfo(d.id, 'workers'); html += `
${d.name}
${d.supplierName}s: ${suppliers} M: ${suppliers}/${nextSupM} (${supMilestoneM}x) (+${fmtNum(autoSupplySpeed)} r/s) • ${d.workerName}s: ${workers} M: ${workers}/${nextWorkM} (${workMilestoneM}x) (${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 dealers = data.dealers || 0; const dealMilestoneM = getUnitMilestoneMultiplier(dealers); const allTierDealM = getAllTierMilestoneMultiplier('dealers'); const dealInfo = getUnitPurchaseInfo(d.id, 'dealers'); const nextDealM = getNextMilestone(dealers); const unitPrice = Math.floor(d.sellPrice * getSellPriceMultiplier()); const dealSpeed = dealers * d.sellRate * getDealerSpeedMultiplier() * dealMilestoneM * allTierDealM; html += `
${d.name}
${d.dealerName}s: ${dealers} M: ${dealers}/${nextDealM} (${dealMilestoneM}x)
$${unitPrice.toLocaleString()}/g
In Stash ${fmtNum(data.product)}g
Deal Speed ${fmtNum(dealSpeed)}/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; } // 3. TROPHIES VIEW (Multi-Tiered Achievements) function renderTrophiesView() { let html = `
SYNDICATE TROPHIES & CAREER MILESTONES CAREER VOL: $${fmtNum(G.totalEarned)}
`; ACHIEVEMENT_TIERS.forEach(ach => { const currentTier = (G.achievements && G.achievements[ach.id]) || 0; const maxTier = ach.tiers.length; const isMaxed = currentTier >= maxTier; // Current stat progress let currentVal = 0; if (ach.statKey === 'totalEarned') currentVal = G.totalEarned || 0; else if (ach.statKey === 'cash') currentVal = G.cash || 0; else if (ach.statKey === 'totalPersonnel') { currentVal = Object.values(G.drugs || {}).reduce((sum, d) => sum + (d.suppliers || 0) + (d.workers || 0) + (d.dealers || 0), 0); } else if (ach.statKey === 'unlockedCount') { currentVal = Object.values(G.drugs || {}).filter(d => d.unlocked).length; } else { currentVal = (G.stats && G.stats[ach.statKey]) || 0; } const nextTierInfo = isMaxed ? ach.tiers[maxTier - 1] : ach.tiers[currentTier]; const nextReq = nextTierInfo.req; const progressPct = isMaxed ? 100 : Math.min(100, Math.floor((currentVal / nextReq) * 100)); const canClaim = !isMaxed && currentVal >= nextReq; // Star rating: ★★★★☆ let stars = ''; for (let i = 1; i <= maxTier; i++) { stars += i <= currentTier ? '★' : '☆'; } html += `
${ach.name}
${currentTier > 0 ? ach.tiers[currentTier - 1].name : 'Unranked'}
${stars}
${ach.desc}
${ach.formatValue(currentVal)} / ${ach.formatValue(nextReq)} ${progressPct}%
Reward: ${nextTierInfo.rewardDesc}
${canClaim ? ` ` : isMaxed ? ` MAX TIER REACHED ` : ''}
`; }); dom.viewport.innerHTML = html; } // 4. OFFSHORE (PRESTIGE & SWISS PERK SHOP) function renderOffshoreView() { const totalLifetimeBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0); const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000)); const nextBonds = Math.max(0, bondsToEarn - totalLifetimeBonds); const prodBoostPct = totalLifetimeBonds * 3; const compoundLvl = (G.offshorePerks && G.offshorePerks['compound_interest']) || 0; const salesRate = 4 + (compoundLvl * 2); const salesBoostPct = totalLifetimeBonds * salesRate; 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. Total lifetime bonds grant permanent passive boosts across all runs.

CLEAN BONDS GAINED UPON LAUNDERING:
+${nextBonds} Bonds
Career Volume: $${fmtNum(G.totalEarned)} • Lifetime Bonds: ${totalLifetimeBonds}
Current Passive Boost: +${prodBoostPct}% Production • +${salesBoostPct}% Sales Value
SWISS BLACK MARKET PERKS Spendable: ${G.cleanBonds || 0} Bonds
`; OFFSHORE_PERKS.forEach(perk => { const currentLvl = (G.offshorePerks && G.offshorePerks[perk.id]) || 0; const maxLvl = perk.costs.length; const isMaxed = currentLvl >= maxLvl; const nextCost = isMaxed ? null : perk.costs[currentLvl]; const canBuy = !isMaxed && (G.cleanBonds || 0) >= nextCost; html += `
${perk.name}
${perk.desc}
${currentLvl > 0 ? `LVL ${currentLvl}/${maxLvl}` : 'UNLOCKED 0'}
${currentLvl > 0 ? perk.formatBonus(currentLvl) : 'Inactive'}
`; }); html += `
`; dom.viewport.innerHTML = html; } // --- OPTIMISTIC CLIENT ACTIONS (Debounced Aggregation & Server Reconciliation) --- function queueAction(action) { // Action aggregation: consolidate rapid repetitive clicks if (['buy_raw', 'manufacture', 'sell_manual'].includes(action.type)) { const existing = pendingActions.find(a => a.type === action.type && a.drug_id === action.drug_id); if (existing) { existing.amount = (existing.amount || 1) + (action.amount || 1); scheduleSync(300); return; } } else if (['hire_supplier', 'hire_worker', 'hire_dealer'].includes(action.type)) { const existing = pendingActions.find(a => a.type === action.type && a.drug_id === action.drug_id); if (existing) { existing.count = (existing.count || 1) + (action.count || 1); scheduleSync(300); return; } } pendingActions.push({ ...action }); // Major discrete events flush immediately, micro-actions debounce 300ms if (['launder_prestige', 'bribe_police', 'flush_stash', 'unlock_drug', 'buy_skill', 'blend_recipe'].includes(action.type)) { scheduleSync(0); } else { scheduleSync(300); } } function buyRaw(drugId) { const rawInfo = getRawPurchaseInfo(drugId); if (G.cash >= rawInfo.totalCost && rawInfo.qty > 0) { G.cash -= rawInfo.totalCost; G.drugs[drugId].raw += rawInfo.qty; queueAction({ type: 'buy_raw', drug_id: drugId, amount: rawInfo.qty }); 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); if (!G.stats) G.stats = {}; G.stats.purityProcs = (G.stats.purityProcs || 0) + 1; } data.product += yieldCount; if (!G.stats) G.stats = {}; G.stats.totalProduced = (G.stats.totalProduced || 0) + yieldCount; addHeat(0.5); queueAction({ type: 'manufacture', drug_id: drugId, amount: 1 }); checkAchievements(); 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; if (!G.stats) G.stats = {}; G.stats.totalSold = (G.stats.totalSold || 0) + actual; addHeat(actual * 0.8); queueAction({ type: 'sell_manual', drug_id: drugId, amount: actual }); checkAchievements(); updateLiveInventory(); } } function hireSupplier(drugId) { const supInfo = getUnitPurchaseInfo(drugId, 'suppliers'); if (G.cash >= supInfo.totalCost && supInfo.qty > 0) { G.cash -= supInfo.totalCost; if (!G.drugs[drugId].suppliers) G.drugs[drugId].suppliers = 0; G.drugs[drugId].suppliers += supInfo.qty; queueAction({ type: 'hire_supplier', drug_id: drugId, count: supInfo.qty }); checkAchievements(); renderViewport(); updateLiveInventory(); } } function hireWorker(drugId) { const workInfo = getUnitPurchaseInfo(drugId, 'workers'); if (G.cash >= workInfo.totalCost && workInfo.qty > 0) { G.cash -= workInfo.totalCost; G.drugs[drugId].workers = (G.drugs[drugId].workers || 0) + workInfo.qty; queueAction({ type: 'hire_worker', drug_id: drugId, count: workInfo.qty }); checkAchievements(); renderViewport(); updateLiveInventory(); } } function hireDealer(drugId) { const dealInfo = getUnitPurchaseInfo(drugId, 'dealers'); if (G.cash >= dealInfo.totalCost && dealInfo.qty > 0) { G.cash -= dealInfo.totalCost; G.drugs[drugId].dealers = (G.drugs[drugId].dealers || 0) + dealInfo.qty; queueAction({ type: 'hire_dealer', drug_id: drugId, count: dealInfo.qty }); checkAchievements(); renderViewport(); updateLiveInventory(); } } function claimAchievement(achId, tier) { if (!G.achievements) G.achievements = {}; const currentTier = G.achievements[achId] || 0; if (tier === currentTier + 1) { G.achievements[achId] = tier; queueAction({ type: 'claim_achievement', ach_id: achId, tier }); const achDef = ACHIEVEMENT_TIERS.find(a => a.id === achId); const tierDef = achDef ? achDef.tiers[tier - 1] : null; showToast(`🏆 Claimed: ${achDef.name} - ${tierDef ? tierDef.name : ''}!`); checkAchievements(); renderViewport(); updateLiveInventory(); } } function buyOffshorePerk(perkId) { if (!G.offshorePerks) G.offshorePerks = {}; const perk = OFFSHORE_PERKS.find(p => p.id === perkId); if (!perk) return; const currentLvl = G.offshorePerks[perkId] || 0; if (currentLvl >= perk.costs.length) return; const cost = perk.costs[currentLvl]; if ((G.cleanBonds || 0) >= cost) { G.cleanBonds -= cost; G.offshorePerks[perkId] = currentLvl + 1; queueAction({ type: 'buy_offshore_perk', perk_id: perkId }); showToast(`Invested in ${perk.name} (Lvl ${currentLvl + 1})!`); 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; // Check Procurement Pipeline perk const starterUnits = (G.offshorePerks && G.offshorePerks['auto_contracts']) || 0; if (starterUnits > 0) { G.drugs[drugId].suppliers = (G.drugs[drugId].suppliers || 0) + starterUnits; G.drugs[drugId].workers = (G.drugs[drugId].workers || 0) + starterUnits; } 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 totalLifetimeBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0); const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000)); const gain = Math.max(0, bondsToEarn - totalLifetimeBonds); if (gain < 1) return; if (confirm(`Launder all assets for +${gain} Clean Offshore Bonds? Your syndicate operations will reset with permanent perks retained.`)) { queueAction({ type: 'launder_prestige' }); G.totalCleanBonds = totalLifetimeBonds + gain; G.cleanBonds = (G.cleanBonds || 0) + gain; if (!G.stats) G.stats = {}; G.stats.prestigeCount = (G.stats.prestigeCount || 0) + 1; let startCash = 50.0 + (G.totalCleanBonds * 25.0); const swissTier = (G.achievements && G.achievements['swiss_kingpin']) || 0; if (swissTier >= 1) startCash += 250.0; if (swissTier >= 2) startCash += 2500.0; if (swissTier >= 3) startCash += 25000.0; if (swissTier >= 4) startCash += 250000.0; G.cash = startCash; G.heat = 0.0; const starterUnits = (G.offshorePerks && G.offshorePerks['auto_contracts']) || 0; DRUG_CATALOG.forEach(d => { G.drugs[d.id] = { unlocked: d.unlockedDefault, raw: 0.0, product: 0.0, suppliers: d.unlockedDefault ? starterUnits : 0, workers: d.unlockedDefault ? starterUnits : 0, dealers: 0 }; }); checkAchievements(); showToast(`Laundered! You now hold ${G.cleanBonds} Spendable Bonds ($${startCash.toLocaleString()} starting cash).`); 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 & BADGE UPDATE --- function checkAchievements() { let unreadCount = 0; ACHIEVEMENT_TIERS.forEach(ach => { const currentTier = (G.achievements && G.achievements[ach.id]) || 0; const maxTier = ach.tiers.length; if (currentTier < maxTier) { let currentVal = 0; if (ach.statKey === 'totalEarned') currentVal = G.totalEarned || 0; else if (ach.statKey === 'cash') currentVal = G.cash || 0; else if (ach.statKey === 'totalPersonnel') { currentVal = Object.values(G.drugs || {}).reduce((sum, d) => sum + (d.suppliers || 0) + (d.workers || 0) + (d.dealers || 0), 0); } else if (ach.statKey === 'unlockedCount') { currentVal = Object.values(G.drugs || {}).filter(d => d.unlocked).length; } else { currentVal = (G.stats && G.stats[ach.statKey]) || 0; } const nextReq = ach.tiers[currentTier].req; if (currentVal >= nextReq) { unreadCount += 1; } } }); if (dom.trophyBadge) { if (unreadCount > 0) { dom.trophyBadge.textContent = unreadCount; dom.trophyBadge.style.display = 'block'; } else { dom.trophyBadge.style.display = 'none'; } } } // --- 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(); const supMulti = getSupplierSpeedMultiplier(); const workMulti = getWorkerSpeedMultiplier(); const allTierSupM = getAllTierMilestoneMultiplier('suppliers'); const allTierWorkM = getAllTierMilestoneMultiplier('workers'); const allTierDealM = getAllTierMilestoneMultiplier('dealers'); 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 supMilestoneM = getUnitMilestoneMultiplier(suppliers); const supplyRate = d.supplyRate || 1.0; const toSupply = suppliers * supplyRate * prodMulti * supMulti * supMilestoneM * allTierSupM * deltaSec; data.raw = data.raw + toSupply; if (!G.stats) G.stats = {}; G.stats.totalSupplied = (G.stats.totalSupplied || 0) + toSupply; } // Auto Production const workers = data.workers || 0; if (workers > 0 && data.raw > 0) { const workMilestoneM = getUnitMilestoneMultiplier(workers); const prodPerSec = workers * d.produceRate * prodMulti * workMulti * workMilestoneM * allTierWorkM; const toMake = Math.min(data.raw, prodPerSec * deltaSec); data.raw = Math.max(0, data.raw - toMake); data.product = data.product + toMake; if (!G.stats) G.stats = {}; G.stats.totalProduced = (G.stats.totalProduced || 0) + toMake; } // Auto Sales const dealers = data.dealers || 0; if (dealers > 0 && data.product > 0) { const dealMilestoneM = getUnitMilestoneMultiplier(dealers); const sellPerSec = dealers * d.sellRate * dealerSpeed * dealMilestoneM * allTierDealM; 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.stats) G.stats = {}; G.stats.totalSold = (G.stats.totalSold || 0) + toSell; } }); 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 DOM text updates updateLiveInventory(); } function updateLiveInventory() { dom.cash.textContent = fmtNum(G.cash); dom.rate.textContent = fmtNum(cashRateTracker); dom.bonds.textContent = fmtInt(G.cleanBonds || 0); const totalLifetimeBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0); const compoundLvl = (G.offshorePerks && G.offshorePerks['compound_interest']) || 0; const bondRate = 4 + (compoundLvl * 2); dom.multiplier.textContent = fmtInt(totalLifetimeBonds * bondRate); dom.heatPct.textContent = `${Math.round(G.heat)}%`; dom.heatBar.style.width = `${Math.min(100, Math.round(G.heat))}%`; const prodMulti = getProductionMultiplier(); const supMulti = getSupplierSpeedMultiplier(); const workMulti = getWorkerSpeedMultiplier(); const allTierSupM = getAllTierMilestoneMultiplier('suppliers'); const allTierWorkM = getAllTierMilestoneMultiplier('workers'); 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 suppliers = drugData.suppliers || 0; const supMilestoneM = getUnitMilestoneMultiplier(suppliers); const currentSupplySpeed = suppliers * (d.supplyRate || 1.0) * prodMulti * supMulti * supMilestoneM * allTierSupM; autoSupplyEl.textContent = `(+${fmtNum(currentSupplySpeed)} r/s)`; autoSupplyEl.style.color = suppliers > 0 ? '#38bdf8' : '#8b949e'; } const workerCountEl = document.getElementById(`val-worker-count-${d.id}`); if (workerCountEl) workerCountEl.textContent = `${d.workerName}s: ${drugData.workers || 0}`; const autoProdEl = document.getElementById(`val-auto-prod-${d.id}`); if (autoProdEl) { const workers = drugData.workers || 0; const workMilestoneM = getUnitMilestoneMultiplier(workers); const currentSpeed = workers * d.produceRate * prodMulti * workMulti * workMilestoneM * allTierWorkM; autoProdEl.textContent = `(${fmtNum(currentSpeed)}g/s)`; autoProdEl.style.color = (workers > 0 && drugData.raw > 0) ? '#4ade80' : '#8b949e'; } // Multi-buy pricing & button states const rawInfo = getRawPurchaseInfo(d.id); const supInfo = getUnitPurchaseInfo(d.id, 'suppliers'); const workInfo = getUnitPurchaseInfo(d.id, 'workers'); const dealInfo = getUnitPurchaseInfo(d.id, 'dealers'); const btnBuyRaw = document.getElementById(`btn-buy-raw-${d.id}`); if (btnBuyRaw) { btnBuyRaw.disabled = (G.cash < rawInfo.totalCost); btnBuyRaw.innerHTML = `Buy Raw (+${rawInfo.qty})$${rawInfo.totalCost.toLocaleString()}`; } const btnHireSupplier = document.getElementById(`btn-hire-supplier-${d.id}`); if (btnHireSupplier) { btnHireSupplier.disabled = (G.cash < supInfo.totalCost); btnHireSupplier.innerHTML = `Hire ${d.supplierName} (+${supInfo.qty})$${supInfo.totalCost.toLocaleString()}`; } 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 < workInfo.totalCost); btnHireWorker.innerHTML = `Hire ${d.workerName} (+${workInfo.qty})$${workInfo.totalCost.toLocaleString()}`; } const btnHireDealer = document.getElementById(`btn-hire-dealer-${d.id}`); if (btnHireDealer) { btnHireDealer.disabled = (G.cash < dealInfo.totalCost); btnHireDealer.innerHTML = `Hire ${d.dealerName} (+${dealInfo.qty})$${dealInfo.totalCost.toLocaleString()}`; } 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 SYNC & RECONCILIATION --- function scheduleSync(delayMs = 300) { if (delayMs === 0) { if (syncDebounceTimer) { clearTimeout(syncDebounceTimer); syncDebounceTimer = null; } triggerServerSync(); return; } if (syncDebounceTimer) clearTimeout(syncDebounceTimer); syncDebounceTimer = setTimeout(() => { syncDebounceTimer = null; triggerServerSync(); }, delayMs); } function applyLocalActions(state, actions) { const rawCostMulti = getRawCostMultiplier(); const sellPriceMulti = getSellPriceMultiplier(); actions.forEach(act => { const dId = act.drug_id; const dCfg = DRUG_CATALOG.find(x => x.id === dId); if (act.type === 'claim_achievement') { if (!state.achievements) state.achievements = {}; state.achievements[act.ach_id] = act.tier; return; } if (act.type === 'buy_offshore_perk') { if (!state.offshorePerks) state.offshorePerks = {}; const curLvl = state.offshorePerks[act.perk_id] || 0; state.offshorePerks[act.perk_id] = curLvl + 1; return; } if (!state.drugs || !state.drugs[dId]) return; if (act.type === 'buy_raw' && dCfg) { const amount = act.amount || 1; const unitCost = Math.floor(dCfg.rawCost * rawCostMulti); const total = unitCost * amount; if (state.cash >= total) { state.cash -= total; state.drugs[dId].raw += amount; } } else if (act.type === 'manufacture') { const amount = act.amount || 1; const actual = Math.min(amount, Math.floor(state.drugs[dId].raw)); if (actual > 0) { state.drugs[dId].raw -= actual; state.drugs[dId].product += actual; } } else if (act.type === 'sell_manual' && dCfg) { const amount = act.amount || 1; const actual = Math.min(amount, Math.floor(state.drugs[dId].product)); if (actual > 0) { state.drugs[dId].product -= actual; const profit = actual * Math.floor(dCfg.sellPrice * sellPriceMulti); state.cash += profit; state.totalEarned += profit; } } else if (act.type === 'hire_supplier' && dCfg) { const count = act.count || 1; const baseCost = dCfg.supplierCost || 75; for (let i = 0; i < count; i++) { const currentCount = state.drugs[dId].suppliers || 0; const cost = Math.floor(baseCost * Math.pow(getPriceScaling(), currentCount)); if (state.cash >= cost) { state.cash -= cost; state.drugs[dId].suppliers = currentCount + 1; } } } else if (act.type === 'hire_worker' && dCfg) { const count = act.count || 1; const baseCost = dCfg.workerCost; for (let i = 0; i < count; i++) { const currentCount = state.drugs[dId].workers || 0; const cost = Math.floor(baseCost * Math.pow(getPriceScaling(), currentCount)); if (state.cash >= cost) { state.cash -= cost; state.drugs[dId].workers = currentCount + 1; } } } else if (act.type === 'hire_dealer' && dCfg) { const count = act.count || 1; const baseCost = dCfg.dealerCost; for (let i = 0; i < count; i++) { const currentCount = state.drugs[dId].dealers || 0; const cost = Math.floor(baseCost * Math.pow(getPriceScaling(), currentCount)); if (state.cash >= cost) { state.cash -= cost; state.drugs[dId].dealers = currentCount + 1; } } } }); } async function triggerServerSync() { if (isSyncing) { // Postpone flush while another network request is in flight scheduleSync(200); return; } const actionsToSend = [...pendingActions]; pendingActions = []; inFlightActions = [...actionsToSend]; actionSeq += 1; const currentSeq = actionSeq; isSyncing = true; if (API.getToken()) { const response = await API.syncTick(actionsToSend, currentSeq); if (response && response.save_data) { const serverState = response.save_data; // Reconcile: Take authoritative server state as base const reconciled = { ...INITIAL_GAME_STATE, ...serverState, buyMode: G.buyMode, activeTab: G.activeTab }; // In-flight actions have been committed to the server inFlightActions = []; // Apply any new actions that user clicked while the request was in flight if (pendingActions.length > 0) { applyLocalActions(reconciled, pendingActions); } G = reconciled; checkAchievements(); updateLiveInventory(); } else { // Network failure / offline: merge inFlight back to pending pendingActions = [...inFlightActions, ...pendingActions]; inFlightActions = []; } } else { // Guest mode / offline fallback: persist locally inFlightActions = []; localStorage.setItem('el_puro_local_save', JSON.stringify(G)); } isSyncing = false; // If new actions arrived during sync, flush them if (pendingActions.length > 0) { scheduleSync(300); } } // 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); } // Background heartbeat (every 10 seconds): syncs idle accrual when player is not actively clicking setInterval(() => { if (!syncDebounceTimer && !isSyncing) { triggerServerSync(); } }, 10000); // 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)); }); // Setup Buy Multiplier Pill Buttons document.querySelectorAll('.btn-multi-pill').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-multi-pill').forEach(b => b.classList.remove('active')); btn.classList.add('active'); G.buyMode = btn.dataset.mode; renderViewport(); updateLiveInventory(); }); }); // 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(); checkAchievements(); updateLiveInventory(); requestAnimationFrame(gameLoop); });