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 {
font-size: 0.7rem;
font-size: 0.72rem;
font-weight: 700;
color: var(--emerald-dark);
margin-left: 2px;
letter-spacing: -0.02em;
white-space: nowrap;
}
/* Sector Output Summary Banner */
@ -577,30 +579,35 @@ body {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 8px 12px;
padding: 6px 8px;
}
.sector-tabs-scroll {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 2px;
gap: 6px;
width: 100%;
}
.sector-pill {
flex: 1 1 0;
min-width: 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
padding: 6px 12px;
padding: 8px 6px;
border-radius: var(--radius-sm);
font-size: 0.78rem;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
box-sizing: border-box;
}
.sector-pill iconify-icon {
font-size: 1.15rem;
flex-shrink: 0;
}
.sector-pill:hover {
@ -613,6 +620,14 @@ body {
border-color: var(--text-primary);
}
.sector-pill.active iconify-icon {
color: #ffffff !important;
}
.sector-pill.active .sector-pill-rate {
color: #a7f3d0;
}
.production-grid {
display: grid;
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.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.sync.queueAction(action);
this.mountActiveView();
@ -342,9 +352,12 @@ class ProductionApp {
const elContractsBadge = document.getElementById('contracts-badge');
if (elContractsBadge) {
const count = (state.activeContracts || []).length;
elContractsBadge.textContent = count;
elContractsBadge.style.display = count > 0 ? 'inline-flex' : 'none';
const readyCount = state.readyContractsCount || 0;
const textVal = String(readyCount);
if (elContractsBadge.textContent !== textVal) {
elContractsBadge.textContent = textVal;
}
elContractsBadge.style.display = readyCount > 0 ? 'inline-flex' : 'none';
}
const userBadge = document.getElementById('user-badge');

View file

@ -193,18 +193,35 @@ export class SimulationEngine {
updateContracts(dt) {
const state = this.sm.state;
if (!state.activeContracts) state.activeContracts = [];
if (state.contractCooldown === undefined) state.contractCooldown = 0;
for (let i = state.activeContracts.length - 1; i >= 0; i--) {
const c = state.activeContracts[i];
c.remainingSeconds -= dt;
if (c.remainingSeconds <= 0) {
// Expired contract, remove
// Expired contract, remove and trigger a cooldown before next tender
state.activeContracts.splice(i, 1);
state.contractCooldown = Math.max(state.contractCooldown, 10.0);
}
}
// Replenish up to 3 open contracts
if (state.activeContracts.length < 3) {
// Count how many active tenders are fulfilled and ready to collect
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);
if (unlockedProducts.length > 0) {
const availableTemplates = CONTRACT_TEMPLATES.filter(tmpl =>
@ -226,6 +243,11 @@ export class SimulationEngine {
rewardCash: tmpl.rewardCash,
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>
</div>
<div class="contracts-meta-badge">
<span>Completed Tenders:</span>
<strong id="contract-stats-completed">${state.stats.contractsCompleted || 0}</strong>
<span>Ready to Deliver:</span>
<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>
@ -125,6 +127,18 @@ export function updateContractsBoard(container, state, onAction) {
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) {
const currentStock = state.products[c.productId]?.stock || 0;
const canFulfill = currentStock >= c.targetAmount;

View file

@ -69,17 +69,16 @@ export function mountProductionFloor(container, state, onAction, engine) {
let html = `
<div class="production-toolbar">
<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>
<span>All Sectors</span>
<span class="sector-pill-rate font-mono" id="pill-rate-all">+$${fmtNum(engine?.cashRate || 0, 1)}/s</span>
</button>
${SECTORS.map(s => {
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 };
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>
<span>${s.name}</span>
<span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 2)}/s</span>
<span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 1)}/s</span>
</button>
`;
}).join('')}
@ -373,12 +372,18 @@ export function updateProductionFloor(container, state, engine) {
const selectedSector = state.selectedSector || 'all';
// 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) {
const stats = engine?.sectorStats?.[s.id];
if (stats) {
const elPillRate = document.getElementById(`pill-rate-${s.id}`);
if (elPillRate) {
const text = `+$${fmtNum(stats.revenueRate, 2)}/s`;
const text = `+$${fmtNum(stats.revenueRate, 1)}/s`;
if (elPillRate.textContent !== text) elPillRate.textContent = text;
}
// Update All-Sectors Mini Cards if on 'all'