feat(ui): evenly distribute sector pills with icons and compact rates, add 10s tender cooldown, and sync ready tender counter

This commit is contained in:
Christian 2026-10-15 23:49:46 +02:00
parent e5232c2af3
commit da5435afbc
5 changed files with 92 additions and 23 deletions

View file

@ -385,10 +385,12 @@ body {
} }
.sector-pill-rate { .sector-pill-rate {
font-size: 0.7rem; font-size: 0.72rem;
font-weight: 700; font-weight: 700;
color: var(--emerald-dark); color: var(--emerald-dark);
margin-left: 2px; margin-left: 2px;
letter-spacing: -0.02em;
white-space: nowrap;
} }
/* Sector Output Summary Banner */ /* Sector Output Summary Banner */
@ -577,30 +579,35 @@ body {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: 8px 12px; padding: 6px 8px;
} }
.sector-tabs-scroll { .sector-tabs-scroll {
display: flex; display: flex;
gap: 8px; gap: 6px;
overflow-x: auto; width: 100%;
padding-bottom: 2px;
} }
.sector-pill { .sector-pill {
flex: 1 1 0;
min-width: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
gap: 6px; gap: 6px;
background: var(--bg-surface-sub); background: var(--bg-surface-sub);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
padding: 6px 12px; padding: 8px 6px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-size: 0.78rem;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: all 0.15s; transition: all 0.15s;
box-sizing: border-box;
}
.sector-pill iconify-icon {
font-size: 1.15rem;
flex-shrink: 0;
} }
.sector-pill:hover { .sector-pill:hover {
@ -613,6 +620,14 @@ body {
border-color: var(--text-primary); border-color: var(--text-primary);
} }
.sector-pill.active iconify-icon {
color: #ffffff !important;
}
.sector-pill.active .sector-pill-rate {
color: #a7f3d0;
}
.production-grid { .production-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));

View file

@ -256,6 +256,16 @@ class ProductionApp {
state.stats.contractsCompleted = (state.stats.contractsCompleted || 0) + 1; state.stats.contractsCompleted = (state.stats.contractsCompleted || 0) + 1;
state.activeContracts.splice(idx, 1); state.activeContracts.splice(idx, 1);
state.contractCooldown = 10.0; // 10 second cooldown before new tender RFP appears
// Recompute readyContractsCount
let readyCount = 0;
for (const actC of state.activeContracts) {
const ps = state.products[actC.productId];
if (ps && (ps.stock || 0) >= actC.targetAmount) readyCount++;
}
state.readyContractsCount = readyCount;
this.showToast(`Contract Fulfilled! Earned ${fmtCurrency(totalPayout)} + ${c.rewardRep} Rep!`, 'success'); this.showToast(`Contract Fulfilled! Earned ${fmtCurrency(totalPayout)} + ${c.rewardRep} Rep!`, 'success');
this.sync.queueAction(action); this.sync.queueAction(action);
this.mountActiveView(); this.mountActiveView();
@ -342,9 +352,12 @@ class ProductionApp {
const elContractsBadge = document.getElementById('contracts-badge'); const elContractsBadge = document.getElementById('contracts-badge');
if (elContractsBadge) { if (elContractsBadge) {
const count = (state.activeContracts || []).length; const readyCount = state.readyContractsCount || 0;
elContractsBadge.textContent = count; const textVal = String(readyCount);
elContractsBadge.style.display = count > 0 ? 'inline-flex' : 'none'; if (elContractsBadge.textContent !== textVal) {
elContractsBadge.textContent = textVal;
}
elContractsBadge.style.display = readyCount > 0 ? 'inline-flex' : 'none';
} }
const userBadge = document.getElementById('user-badge'); const userBadge = document.getElementById('user-badge');

View file

@ -193,18 +193,35 @@ export class SimulationEngine {
updateContracts(dt) { updateContracts(dt) {
const state = this.sm.state; const state = this.sm.state;
if (!state.activeContracts) state.activeContracts = []; if (!state.activeContracts) state.activeContracts = [];
if (state.contractCooldown === undefined) state.contractCooldown = 0;
for (let i = state.activeContracts.length - 1; i >= 0; i--) { for (let i = state.activeContracts.length - 1; i >= 0; i--) {
const c = state.activeContracts[i]; const c = state.activeContracts[i];
c.remainingSeconds -= dt; c.remainingSeconds -= dt;
if (c.remainingSeconds <= 0) { if (c.remainingSeconds <= 0) {
// Expired contract, remove // Expired contract, remove and trigger a cooldown before next tender
state.activeContracts.splice(i, 1); state.activeContracts.splice(i, 1);
state.contractCooldown = Math.max(state.contractCooldown, 10.0);
} }
} }
// Replenish up to 3 open contracts // Count how many active tenders are fulfilled and ready to collect
if (state.activeContracts.length < 3) { let readyCount = 0;
for (const c of state.activeContracts) {
const pState = state.products[c.productId];
if (pState && (pState.stock || 0) >= c.targetAmount) {
readyCount++;
}
}
state.readyContractsCount = readyCount;
// Process replenishment cooldown
if (state.contractCooldown > 0) {
state.contractCooldown = Math.max(0, state.contractCooldown - dt);
}
// Replenish up to 3 open contracts only when cooldown is over
if (state.activeContracts.length < 3 && state.contractCooldown <= 0) {
const unlockedProducts = PRODUCTS.filter(p => state.products[p.id]?.unlocked); const unlockedProducts = PRODUCTS.filter(p => state.products[p.id]?.unlocked);
if (unlockedProducts.length > 0) { if (unlockedProducts.length > 0) {
const availableTemplates = CONTRACT_TEMPLATES.filter(tmpl => const availableTemplates = CONTRACT_TEMPLATES.filter(tmpl =>
@ -226,6 +243,11 @@ export class SimulationEngine {
rewardCash: tmpl.rewardCash, rewardCash: tmpl.rewardCash,
rewardRep: tmpl.rewardRep rewardRep: tmpl.rewardRep
}); });
// If still under 3, stagger next replenish with a slight pause
if (state.activeContracts.length < 3) {
state.contractCooldown = 10.0;
}
} }
} }
} }

View file

@ -23,8 +23,10 @@ export function mountContractsBoard(container, state, onAction) {
</p> </p>
</div> </div>
<div class="contracts-meta-badge"> <div class="contracts-meta-badge">
<span>Completed Tenders:</span> <span>Ready to Deliver:</span>
<strong id="contract-stats-completed">${state.stats.contractsCompleted || 0}</strong> <strong id="contract-stats-ready" class="font-mono text-profit">${state.readyContractsCount || 0}</strong>
<span style="margin-left: 8px; border-left: 1px solid var(--border-color); padding-left: 8px;">Completed:</span>
<strong id="contract-stats-completed" class="font-mono">${state.stats.contractsCompleted || 0}</strong>
</div> </div>
</div> </div>
@ -125,6 +127,18 @@ export function updateContractsBoard(container, state, onAction) {
return; return;
} }
const elStatsReady = document.getElementById('contract-stats-ready');
if (elStatsReady) {
const val = String(state.readyContractsCount || 0);
if (elStatsReady.textContent !== val) elStatsReady.textContent = val;
}
const elStatsCompleted = document.getElementById('contract-stats-completed');
if (elStatsCompleted) {
const val = String(state.stats?.contractsCompleted || 0);
if (elStatsCompleted.textContent !== val) elStatsCompleted.textContent = val;
}
for (const c of contracts) { for (const c of contracts) {
const currentStock = state.products[c.productId]?.stock || 0; const currentStock = state.products[c.productId]?.stock || 0;
const canFulfill = currentStock >= c.targetAmount; const canFulfill = currentStock >= c.targetAmount;

View file

@ -69,17 +69,16 @@ export function mountProductionFloor(container, state, onAction, engine) {
let html = ` let html = `
<div class="production-toolbar"> <div class="production-toolbar">
<div class="sector-tabs-scroll"> <div class="sector-tabs-scroll">
<button class="sector-pill ${selectedSector === 'all' ? 'active' : ''}" data-sector="all"> <button class="sector-pill ${selectedSector === 'all' ? 'active' : ''}" data-sector="all" title="All Sectors">
<iconify-icon icon="lucide:layout-grid"></iconify-icon> <iconify-icon icon="lucide:layout-grid"></iconify-icon>
<span>All Sectors</span> <span class="sector-pill-rate font-mono" id="pill-rate-all">+$${fmtNum(engine?.cashRate || 0, 1)}/s</span>
</button> </button>
${SECTORS.map(s => { ${SECTORS.map(s => {
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 }; const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 };
return ` return `
<button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}"> <button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}" title="${s.name}">
<iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon> <iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon>
<span>${s.name}</span> <span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 1)}/s</span>
<span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 2)}/s</span>
</button> </button>
`; `;
}).join('')} }).join('')}
@ -373,12 +372,18 @@ export function updateProductionFloor(container, state, engine) {
const selectedSector = state.selectedSector || 'all'; const selectedSector = state.selectedSector || 'all';
// 1. Update Sector Navigation Pills Output Rates // 1. Update Sector Navigation Pills Output Rates
const elPillRateAll = document.getElementById('pill-rate-all');
if (elPillRateAll) {
const text = `+$${fmtNum(engine?.cashRate || 0, 1)}/s`;
if (elPillRateAll.textContent !== text) elPillRateAll.textContent = text;
}
for (const s of SECTORS) { for (const s of SECTORS) {
const stats = engine?.sectorStats?.[s.id]; const stats = engine?.sectorStats?.[s.id];
if (stats) { if (stats) {
const elPillRate = document.getElementById(`pill-rate-${s.id}`); const elPillRate = document.getElementById(`pill-rate-${s.id}`);
if (elPillRate) { if (elPillRate) {
const text = `+$${fmtNum(stats.revenueRate, 2)}/s`; const text = `+$${fmtNum(stats.revenueRate, 1)}/s`;
if (elPillRate.textContent !== text) elPillRate.textContent = text; if (elPillRate.textContent !== text) elPillRate.textContent = text;
} }
// Update All-Sectors Mini Cards if on 'all' // Update All-Sectors Mini Cards if on 'all'