feat(ui): add visual sync countdown bar, quick stash overview strip, and live button state reactive updates
This commit is contained in:
parent
0a7fb7322c
commit
2fdc030e9d
3 changed files with 209 additions and 18 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,29 @@
|
|||
<div id="heat-bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Sync Countdown Bar -->
|
||||
<div id="sync-container">
|
||||
<div class="sync-label-row">
|
||||
<span class="sync-status-text">
|
||||
<span class="sync-pulse-dot"></span> NEXT SERVER TICK
|
||||
</span>
|
||||
<span id="sync-timer-text">6.0s</span>
|
||||
</div>
|
||||
<div class="sync-bar-track">
|
||||
<div id="sync-bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Inventory Overview Strip (Always visible) -->
|
||||
<div id="quick-stash-strip">
|
||||
<div class="stash-strip-title">
|
||||
<span>STASH OVERVIEW:</span>
|
||||
</div>
|
||||
<div id="quick-stash-items" class="stash-strip-items">
|
||||
<!-- Dynamically populated chips of all owned products -->
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Toast Notifications Container -->
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
|
||||
<div class="btn-grid-actions">
|
||||
<button class="btn-tactical" onclick="buyRaw('${d.id}')" ${G.cash < rawCost ? 'disabled' : ''}>
|
||||
<button id="btn-buy-raw-${d.id}" class="btn-tactical" onclick="buyRaw('${d.id}')" ${G.cash < rawCost ? 'disabled' : ''}>
|
||||
Buy Raw
|
||||
<span class="btn-subtext">$${rawCost.toLocaleString()}</span>
|
||||
</button>
|
||||
<button class="btn-tactical btn-primary-action" onclick="manufacture('${d.id}')" ${data.raw < 1 ? 'disabled' : ''}>
|
||||
<button id="btn-make-${d.id}" class="btn-tactical btn-primary-action" onclick="manufacture('${d.id}')" ${data.raw < 1 ? 'disabled' : ''}>
|
||||
Synthesize
|
||||
<span class="btn-subtext">1 Raw -> 1g</span>
|
||||
</button>
|
||||
<button class="btn-tactical btn-sell-action" onclick="sellManual('${d.id}')" ${data.product < 1 ? 'disabled' : ''}>
|
||||
<button id="btn-sell-${d.id}" class="btn-tactical btn-sell-action" onclick="sellManual('${d.id}')" ${data.product < 1 ? 'disabled' : ''}>
|
||||
Quick Deal
|
||||
<span class="btn-subtext">+$${Math.floor(d.sellPrice * getSellPriceMultiplier()).toLocaleString()}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-grid-auto">
|
||||
<button class="btn-tactical" onclick="hireWorker('${d.id}')" ${G.cash < workerCost ? 'disabled' : ''}>
|
||||
<button id="btn-hire-worker-${d.id}" class="btn-tactical" onclick="hireWorker('${d.id}')" ${G.cash < workerCost ? 'disabled' : ''}>
|
||||
Hire ${d.workerName}
|
||||
<span class="btn-subtext">$${workerCost.toLocaleString()}</span>
|
||||
</button>
|
||||
|
|
@ -291,11 +294,11 @@ function renderStreetsView() {
|
|||
</div>
|
||||
|
||||
<div class="btn-grid-auto">
|
||||
<button class="btn-tactical btn-sell-action" onclick="hireDealer('${d.id}')" ${G.cash < dealerCost ? 'disabled' : ''}>
|
||||
<button id="btn-hire-dealer-${d.id}" class="btn-tactical btn-sell-action" onclick="hireDealer('${d.id}')" ${G.cash < dealerCost ? 'disabled' : ''}>
|
||||
Hire ${d.dealerName}
|
||||
<span class="btn-subtext">$${dealerCost.toLocaleString()}</span>
|
||||
</button>
|
||||
<button class="btn-tactical" onclick="sellManual('${d.id}', 10)" ${data.product < 10 ? 'disabled' : ''}>
|
||||
<button id="btn-sell-10x-${d.id}" class="btn-tactical" onclick="sellManual('${d.id}', 10)" ${data.product < 10 ? 'disabled' : ''}>
|
||||
Sell 10x Stash
|
||||
<span class="btn-subtext">+$${(unitPrice * Math.min(10, Math.floor(data.product))).toLocaleString()}</span>
|
||||
</button>
|
||||
|
|
@ -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 += `
|
||||
<div class="stash-chip" title="${d.name}">
|
||||
<span class="stash-chip-dot" style="background: ${d.color};"></span>
|
||||
<span>${d.name.split(' ')[0]}:</span>
|
||||
<span class="stash-chip-val" style="color: ${drugData.product > 0 ? '#fff' : '#6b7280'}">${fmtNum(drugData.product)}g</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 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 || '<span style="font-size: 0.65rem; color: #6b7280;">No active stash</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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(() => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue