From aadc21c8c7798187794ebd55f096a6aa5e5e82eb Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 15 Oct 2026 19:07:46 +0200 Subject: [PATCH] feat(ui): eliminate browser alerts/confirms, add custom modal dialogs, and support scientific & idle game notation with 2 decimals --- backend/engine.py | 4 +- frontend/index.html | 74 ++++++++++++++++++- frontend/js/game.js | 171 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 238 insertions(+), 11 deletions(-) diff --git a/backend/engine.py b/backend/engine.py index 19897b7..b4cc4b1 100644 --- a/backend/engine.py +++ b/backend/engine.py @@ -51,10 +51,10 @@ ACHIEVEMENT_TIER_REQS = { } def clean_round(val: float) -> float: - """Rounds float gracefully to 1 decimal place without floating point anomalies.""" + """Rounds float gracefully to 2 decimal places without floating point anomalies.""" if math.isnan(val) or math.isinf(val) or val < 0: return 0.0 - return round(float(val), 1) + return round(float(val), 2) def get_unit_milestone_multiplier(count: int) -> float: """Calculates 2x multiplier for each milestone threshold achieved.""" diff --git a/frontend/index.html b/frontend/index.html index bb7f3b7..19e7ad8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -28,7 +28,10 @@ SECURE -
+
+ [ANON] 15:00
@@ -184,6 +187,75 @@
+ + + + + + diff --git a/frontend/js/game.js b/frontend/js/game.js index cc58360..7ed7547 100644 --- a/frontend/js/game.js +++ b/frontend/js/game.js @@ -60,16 +60,112 @@ 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) --- +// --- USER PREFERENCES (Number Notation: 'standard' vs 'scientific') --- +let userNotation = localStorage.getItem('el_puro_notation') || 'standard'; + +// Large Number Suffixes for "Idle Game Progression" Notation +const NUMBER_SUFFIXES = [ + '', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', + 'Dc', 'Ud', 'Dd', 'Td', 'Qad', 'Qid', 'Sxd', 'Spd', 'Ocd', 'Nod', + 'Vg', 'Uvg', 'Dvg', 'Tvg', 'Qavg', 'Qivg', 'Sxvg', 'Spvg', 'Ocvg', 'Novg', + 'Tg' +]; + +/** + * Formats a number with strictly 2 decimal places. + * Supports: + * 1. 'standard': Idle game notation (K, M, B, T, Qa, Qi...) for >= 1,000,000 or commas for < 1,000,000. + * 2. 'scientific': e.g. 1.25e6, 3.40e12 for >= 1,000,000. + * Numbers < 1,000,000 are always formatted with commas and exactly 2 decimal places. + */ 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 }); + if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0.00'; + const num = Number(val); + if (num === 0) return '0.00'; + const abs = Math.abs(num); + const sign = num < 0 ? '-' : ''; + + if (abs < 1e6) { + return sign + abs.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + if (userNotation === 'scientific') { + const exponent = Math.floor(Math.log10(abs)); + const mantissa = abs / Math.pow(10, exponent); + return sign + mantissa.toFixed(2) + 'e' + exponent; + } + + // Standard Idle Game Progression Notation + const tier = Math.floor(Math.log10(abs) / 3); + if (tier < NUMBER_SUFFIXES.length) { + const scaled = abs / Math.pow(10, tier * 3); + return sign + scaled.toFixed(2) + NUMBER_SUFFIXES[tier]; + } else { + // Fallback to scientific for numbers beyond defined suffixes (> 10^96) + const exponent = Math.floor(Math.log10(abs)); + const mantissa = abs / Math.pow(10, exponent); + return sign + mantissa.toFixed(2) + 'e' + exponent; + } } +/** + * Formats an integer or unit count. + * If >= 1,000,000, formats with 2 decimal places + suffix or exponent. + * If < 1,000,000, formats with integer commas. + */ function fmtInt(val) { if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0'; - return Math.floor(Math.max(0, Number(val))).toLocaleString(); + const num = Math.floor(Math.max(0, Number(val))); + if (num < 1e6) { + return num.toLocaleString(); + } + return fmtNum(num); +} + +// --- CUSTOM MODAL SYSTEM (Replaces window.alert and window.confirm) --- +let activeModalResolve = null; + +function showConfirmModal(title, message, options = {}) { + return new Promise(resolve => { + activeModalResolve = resolve; + const modal = document.getElementById('confirm-modal'); + const titleEl = document.getElementById('confirm-modal-title'); + const msgEl = document.getElementById('confirm-modal-message'); + const iconEl = document.getElementById('confirm-modal-icon'); + const cancelBtn = document.getElementById('btn-confirm-cancel'); + const okBtn = document.getElementById('btn-confirm-ok'); + + if (titleEl) titleEl.textContent = title || 'CONFIRMATION'; + if (msgEl) msgEl.innerHTML = message || 'Proceed with this operation?'; + + if (options.isAlert) { + if (cancelBtn) cancelBtn.style.display = 'none'; + if (okBtn) okBtn.textContent = options.okText || 'Understood'; + if (iconEl) iconEl.setAttribute('icon', 'lucide:info'); + } else { + if (cancelBtn) { + cancelBtn.style.display = 'block'; + cancelBtn.textContent = options.cancelText || 'Cancel'; + } + if (okBtn) okBtn.textContent = options.okText || 'Confirm'; + if (iconEl) iconEl.setAttribute('icon', options.icon || 'lucide:alert-circle'); + } + + modal.classList.add('active'); + }); +} + +function showAlertModal(title, message, options = {}) { + return showConfirmModal(title, message, { ...options, isAlert: true }); +} + +function closeModal(result) { + const modal = document.getElementById('confirm-modal'); + if (modal) modal.classList.remove('active'); + if (activeModalResolve) { + activeModalResolve(result); + activeModalResolve = null; + } } // --- DOM CACHE --- @@ -88,7 +184,9 @@ const dom = { raidBribeCost: document.getElementById('raid-bribe-cost'), toastContainer: document.getElementById('toast-container'), quickStash: document.getElementById('quick-stash-items'), - trophyBadge: document.getElementById('trophy-badge') + trophyBadge: document.getElementById('trophy-badge'), + settingsModal: document.getElementById('settings-modal'), + btnOpenSettings: document.getElementById('btn-open-settings') }; // --- MILESTONES & BULK FORMULAS --- @@ -939,13 +1037,19 @@ function buySkill(skillId) { } } -function launderMoney() { +async 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.`)) { + const confirmed = await showConfirmModal( + 'LAUNDER SYNDICATE ASSETS', + `Launder all operations for +${gain} Clean Offshore Bonds?

Your dirty cash and current equipment will reset, but all permanent Swiss perks, trophies, lifetime bond multipliers and achievements are retained.`, + { okText: 'Launder & Reset', cancelText: 'Abort', icon: 'game-icons:tied-scroll' } + ); + + if (confirmed) { queueAction({ type: 'launder_prestige' }); G.totalCleanBonds = totalLifetimeBonds + gain; G.cleanBonds = (G.cleanBonds || 0) + gain; @@ -1528,6 +1632,57 @@ document.addEventListener('DOMContentLoaded', async () => { } }); + // Confirmation & Alert Modal Button Handlers + const btnConfirmCancel = document.getElementById('btn-confirm-cancel'); + const btnConfirmOk = document.getElementById('btn-confirm-ok'); + if (btnConfirmCancel) btnConfirmCancel.addEventListener('click', () => closeModal(false)); + if (btnConfirmOk) btnConfirmOk.addEventListener('click', () => closeModal(true)); + + // Settings Modal Handling + if (dom.btnOpenSettings && dom.settingsModal) { + dom.btnOpenSettings.addEventListener('click', () => { + // Update radio to match active userNotation + const radios = document.querySelectorAll('input[name="notation-choice"]'); + radios.forEach(r => { + r.checked = (r.value === userNotation); + }); + updateSettingsPreview(); + dom.settingsModal.classList.add('active'); + }); + } + + const btnCloseSettings = document.getElementById('btn-close-settings'); + if (btnCloseSettings && dom.settingsModal) { + btnCloseSettings.addEventListener('click', () => { + const selected = document.querySelector('input[name="notation-choice"]:checked'); + if (selected) { + userNotation = selected.value; + localStorage.setItem('el_puro_notation', userNotation); + } + dom.settingsModal.classList.remove('active'); + renderViewport(); + updateLiveInventory(); + showToast(`Number display updated: ${userNotation === 'scientific' ? 'Scientific' : 'Standard Idle'}`); + }); + } + + document.querySelectorAll('input[name="notation-choice"]').forEach(r => { + r.addEventListener('change', () => { + userNotation = r.value; + localStorage.setItem('el_puro_notation', userNotation); + updateSettingsPreview(); + renderViewport(); + updateLiveInventory(); + }); + }); + + function updateSettingsPreview() { + const previewEl = document.getElementById('settings-preview-val'); + if (previewEl) { + previewEl.textContent = `$1,234,567.89 ➔ $${fmtNum(1234567.89)} | 5.25B ➔ ${fmtNum(5250000000)}`; + } + } + renderViewport(); checkAchievements(); updateLiveInventory();