feat(ui): eliminate browser alerts/confirms, add custom modal dialogs, and support scientific & idle game notation with 2 decimals

This commit is contained in:
Christian 2026-10-15 19:07:46 +02:00
parent 81cf7f2129
commit aadc21c8c7
3 changed files with 238 additions and 11 deletions

View file

@ -51,10 +51,10 @@ ACHIEVEMENT_TIER_REQS = {
} }
def clean_round(val: float) -> float: 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: if math.isnan(val) or math.isinf(val) or val < 0:
return 0.0 return 0.0
return round(float(val), 1) return round(float(val), 2)
def get_unit_milestone_multiplier(count: int) -> float: def get_unit_milestone_multiplier(count: int) -> float:
"""Calculates 2x multiplier for each milestone threshold achieved.""" """Calculates 2x multiplier for each milestone threshold achieved."""

View file

@ -28,7 +28,10 @@
<span class="secure-dot"></span> SECURE <span class="secure-dot"></span> SECURE
</span> </span>
</div> </div>
<div class="status-right"> <div class="status-right" style="display: flex; align-items: center; gap: 8px;">
<button id="btn-open-settings" style="background: none; border: 1px solid #2a3b4c; border-radius: 4px; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 2px 6px; font-size: 0.9rem;" title="Game Settings & Number Notation">
<iconify-icon icon="lucide:settings"></iconify-icon>
</button>
<span id="user-badge" style="cursor: pointer; text-decoration: underline;">[ANON]</span> <span id="user-badge" style="cursor: pointer; text-decoration: underline;">[ANON]</span>
<span id="system-clock">15:00</span> <span id="system-clock">15:00</span>
</div> </div>
@ -184,6 +187,75 @@
</div> </div>
</div> </div>
<!-- Custom Tactical Confirmation / Alert Modal (Replaces browser alert and confirm) -->
<div id="confirm-modal" class="modal-overlay">
<div class="modal-content" style="border-color: var(--brand-gold);">
<div class="modal-header">
<iconify-icon id="confirm-modal-icon" icon="lucide:alert-circle" style="font-size: 1.8rem; color: var(--brand-gold);"></iconify-icon>
<div>
<div id="confirm-modal-title" class="modal-title">CONFIRMATION</div>
<div id="confirm-modal-subtitle" style="font-size: 0.72rem; color: var(--text-secondary);">Syndicate Protocol</div>
</div>
</div>
<p id="confirm-modal-message" style="font-size: 0.82rem; line-height: 1.5; color: var(--text-primary); margin: 4px 0 10px;">
Are you sure you wish to proceed?
</p>
<div id="confirm-modal-actions" style="display: flex; gap: 8px;">
<button id="btn-confirm-cancel" class="btn-tactical" style="flex: 1; padding: 10px;">Cancel</button>
<button id="btn-confirm-ok" class="btn-tactical btn-sell-action" style="flex: 1; padding: 10px;">Confirm</button>
</div>
</div>
</div>
<!-- System Settings & Notation Preferences Modal -->
<div id="settings-modal" class="modal-overlay">
<div class="modal-content" style="border-color: var(--neon-cyan);">
<div class="modal-header">
<iconify-icon icon="lucide:sliders" style="font-size: 1.8rem; color: var(--neon-cyan);"></iconify-icon>
<div>
<div class="modal-title">TERMINAL SETTINGS</div>
<div style="font-size: 0.72rem; color: var(--text-secondary);">Number Display & Preferences</div>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 12px; margin: 6px 0;">
<div>
<div style="font-family: var(--font-mono); font-size: 0.78rem; font-weight: bold; color: var(--text-primary); margin-bottom: 6px;">
NUMBER NOTATION STYLE:
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<label style="display: flex; align-items: center; gap: 10px; background: #0c1218; padding: 10px; border-radius: 8px; border: 1px solid #1e2c3a; cursor: pointer;">
<input type="radio" name="notation-choice" value="standard" style="accent-color: var(--neon-cyan);">
<div>
<div style="font-weight: bold; font-size: 0.82rem; color: #fff;">Idle Game Progression (Recommended)</div>
<div style="font-size: 0.7rem; color: var(--text-secondary); font-family: var(--font-mono);">
e.g. 1.25M, 45.50B, 120.00T, 10.00Qa, 5.00Qi...
</div>
</div>
</label>
<label style="display: flex; align-items: center; gap: 10px; background: #0c1218; padding: 10px; border-radius: 8px; border: 1px solid #1e2c3a; cursor: pointer;">
<input type="radio" name="notation-choice" value="scientific" style="accent-color: var(--neon-cyan);">
<div>
<div style="font-weight: bold; font-size: 0.82rem; color: #fff;">Scientific Notation</div>
<div style="font-size: 0.7rem; color: var(--text-secondary); font-family: var(--font-mono);">
e.g. 1.25e6, 4.55e10, 1.20e14, 5.00e18...
</div>
</div>
</label>
</div>
</div>
<div style="background: #090e13; border: 1px solid #182533; border-radius: 8px; padding: 10px; font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-secondary);">
<span>Precision: </span><span style="color: var(--neon-cyan);">2 Decimal Places</span> (Fixed)<br>
<span>Preview: </span><span id="settings-preview-val" style="color: #4ade80;">$1,234,567.89 ➔ $1.23M</span>
</div>
</div>
<button id="btn-close-settings" class="btn-tactical btn-primary-action" style="padding: 10px;">Save & Return</button>
</div>
</div>
<script src="js/config.js"></script> <script src="js/config.js"></script>
<script src="js/api.js"></script> <script src="js/api.js"></script>
<script src="js/game.js"></script> <script src="js/game.js"></script>

View file

@ -60,16 +60,112 @@ let actionSeq = 0; // Monotonic sequence ID
let syncDebounceTimer = null; // 300ms debounce timer for rapid clicking let syncDebounceTimer = null; // 300ms debounce timer for rapid clicking
let isSyncing = false; // Network in-flight flag 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) { function fmtNum(val) {
if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0'; if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0.00';
const rounded = Math.round(Number(val) * 10) / 10; const num = Number(val);
return rounded.toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 }); 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) { function fmtInt(val) {
if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0'; 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 --- // --- DOM CACHE ---
@ -88,7 +184,9 @@ const dom = {
raidBribeCost: document.getElementById('raid-bribe-cost'), raidBribeCost: document.getElementById('raid-bribe-cost'),
toastContainer: document.getElementById('toast-container'), toastContainer: document.getElementById('toast-container'),
quickStash: document.getElementById('quick-stash-items'), 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 --- // --- 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 totalLifetimeBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0);
const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000)); const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000));
const gain = Math.max(0, bondsToEarn - totalLifetimeBonds); const gain = Math.max(0, bondsToEarn - totalLifetimeBonds);
if (gain < 1) return; 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 <b style="color: var(--neon-cyan);">+${gain} Clean Offshore Bonds</b>?<br><br>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' }); queueAction({ type: 'launder_prestige' });
G.totalCleanBonds = totalLifetimeBonds + gain; G.totalCleanBonds = totalLifetimeBonds + gain;
G.cleanBonds = (G.cleanBonds || 0) + 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(); renderViewport();
checkAchievements(); checkAchievements();
updateLiveInventory(); updateLiveInventory();