feat(ui): eliminate browser alerts/confirms, add custom modal dialogs, and support scientific & idle game notation with 2 decimals
This commit is contained in:
parent
81cf7f2129
commit
aadc21c8c7
3 changed files with 238 additions and 11 deletions
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue