From 2fdc030e9d81bbb252842063bda930e873943052 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 15 Oct 2026 16:34:01 +0200 Subject: [PATCH] feat(ui): add visual sync countdown bar, quick stash overview strip, and live button state reactive updates --- frontend/css/style.css | 105 +++++++++++++++++++++++++++++++++++++++++ frontend/index.html | 23 +++++++++ frontend/js/game.js | 99 +++++++++++++++++++++++++++++++------- 3 files changed, 209 insertions(+), 18 deletions(-) diff --git a/frontend/css/style.css b/frontend/css/style.css index 2702ca4..3d40ce8 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -201,6 +201,111 @@ html, body { transition: width 0.3s ease; } +/* Server Sync Countdown Bar */ +#sync-container { + background: #0b0e12; + padding: 3px 14px 6px; + display: flex; + flex-direction: column; + gap: 3px; + border-top: 1px solid rgba(36, 45, 54, 0.4); +} + +.sync-label-row { + display: flex; + justify-content: space-between; + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-secondary); +} + +.sync-status-text { + display: inline-flex; + align-items: center; + gap: 5px; + letter-spacing: 0.5px; +} + +.sync-pulse-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--neon-cyan); + box-shadow: 0 0 6px var(--neon-cyan); + animation: pulse 1.5s infinite; +} + +.sync-bar-track { + width: 100%; + height: 4px; + background: #151b22; + border-radius: 2px; + overflow: hidden; +} + +#sync-bar-fill { + width: 100%; + height: 100%; + background: linear-gradient(90deg, var(--neon-cyan) 0%, #3b82f6 100%); + transition: width 0.1s linear; +} + +/* Quick Stash Overview Strip (Sticky Under Sync Bar) */ +#quick-stash-strip { + background: #080b0e; + border-top: 1px solid rgba(36, 45, 54, 0.4); + padding: 6px 14px; + display: flex; + align-items: center; + gap: 8px; + overflow-x: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} + +#quick-stash-strip::-webkit-scrollbar { + display: none; +} + +.stash-strip-title { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--text-secondary); + white-space: nowrap; + letter-spacing: 0.5px; + font-weight: 700; +} + +.stash-strip-items { + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.stash-chip { + display: inline-flex; + align-items: center; + gap: 4px; + background: #111720; + border: 1px solid #232e3a; + border-radius: 6px; + padding: 2px 7px; + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--text-primary); +} + +.stash-chip-dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.stash-chip-val { + font-weight: 700; +} + /* Main Viewport Content */ #main-viewport { flex: 1; diff --git a/frontend/index.html b/frontend/index.html index 8cc3ece..3a84979 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -61,6 +61,29 @@
+ + +
+
+ + NEXT SERVER TICK + + 6.0s +
+
+
+
+
+ + +
+
+ STASH OVERVIEW: +
+
+ +
+
diff --git a/frontend/js/game.js b/frontend/js/game.js index 2463668..392a74c 100644 --- a/frontend/js/game.js +++ b/frontend/js/game.js @@ -68,7 +68,10 @@ const dom = { authModal: document.getElementById('auth-modal'), raidModal: document.getElementById('raid-modal'), raidBribeCost: document.getElementById('raid-bribe-cost'), - toastContainer: document.getElementById('toast-container') + 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 --- @@ -219,22 +222,22 @@ function renderLabView() {
- - -
- @@ -291,11 +294,11 @@ function renderStreetsView() {
- - @@ -720,6 +723,24 @@ function clientTick(deltaSec) { // 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() { @@ -730,27 +751,69 @@ function updateLiveInventory() { dom.heatPct.textContent = `${Math.round(G.heat)}%`; dom.heatBar.style.width = `${Math.min(100, Math.round(G.heat))}%`; - // Surgically update raw and product text fields if they exist in DOM + const rawCostMulti = getRawCostMultiplier(); + 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 && G.drugs[d.id]) { - rawEl.textContent = fmtNum(G.drugs[d.id].raw); - } + if (rawEl) rawEl.textContent = fmtNum(drugData.raw); + const prodEl = document.getElementById(`val-prod-${d.id}`); - if (prodEl && G.drugs[d.id]) { - prodEl.textContent = `${fmtNum(G.drugs[d.id].product)}g`; - } + if (prodEl) prodEl.textContent = `${fmtNum(drugData.product)}g`; + const streetProdEl = document.getElementById(`val-prod-street-${d.id}`); - if (streetProdEl && G.drugs[d.id]) { - streetProdEl.textContent = `${fmtNum(G.drugs[d.id].product)}g`; - } + if (streetProdEl) streetProdEl.textContent = `${fmtNum(drugData.product)}g`; + + // Live button state updates (enable/disable as resources change) + const rawCost = Math.floor(d.rawCost * rawCostMulti); + 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 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); }); + + 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]; @@ -788,7 +851,7 @@ function gameLoop(now) { } // Server tick: exactly every 6 seconds to prevent flooding server -setInterval(authoritativeServerTick, 6000); +setInterval(authoritativeServerTick, TICK_INTERVAL_MS); // Clock updates setInterval(() => {