fix(ui): eliminate card jitter on input shortage and display individual factory output metrics

This commit is contained in:
Christian 2026-10-15 22:55:39 +02:00
parent 6a7aa072e4
commit edc9a843b7
2 changed files with 123 additions and 7 deletions

View file

@ -627,10 +627,13 @@ body {
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 410px;
gap: 12px;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
position: relative;
box-sizing: border-box;
}
.facility-card:hover {
@ -652,6 +655,8 @@ body {
display: flex;
gap: 12px;
align-items: flex-start;
min-height: 60px;
box-sizing: border-box;
}
.facility-icon-wrap {
@ -671,20 +676,31 @@ body {
.facility-title-wrap {
flex: 1;
min-width: 0;
overflow: hidden;
}
.facility-meta-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
min-height: 22px;
height: 22px;
flex-wrap: nowrap;
margin-bottom: 2px;
box-sizing: border-box;
}
.sector-badge {
font-size: 0.72rem;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.facility-name {
@ -692,11 +708,17 @@ body {
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.facility-type-sub {
font-size: 0.75rem;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.facility-desc {
@ -714,6 +736,11 @@ body {
font-weight: 700;
padding: 2px 8px;
border-radius: 12px;
white-space: nowrap;
flex-shrink: 0;
height: 20px;
line-height: 1;
box-sizing: border-box;
}
.badge-success {
@ -754,27 +781,40 @@ body {
/* Facility Stats Grid */
.facility-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
background: var(--bg-surface-sub);
border-radius: var(--radius-sm);
padding: 8px 10px;
min-height: 84px;
box-sizing: border-box;
}
.stat-cell {
display: flex;
flex-direction: column;
min-width: 0;
}
.stat-label {
font-size: 0.68rem;
font-size: 0.65rem;
font-weight: 600;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-value {
font-size: 0.8rem;
font-size: 0.78rem;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-warning {
color: var(--amber-dark);
}
/* Inputs Strip */

View file

@ -25,6 +25,34 @@ export function calculateMultiCost(baseCost, currentCount, multiplier) {
return { count: countToBuy, cost: totalCost };
}
/**
* Computes live individual factory output rates (units/s and wholesale revenue $/s)
*/
export function getProductOutputRates(p, pState, state) {
if (!pState || !pState.unlocked || !pState.lines || pState.lines <= 0) {
return { prodRate: 0, revRate: 0, isBottlenecked: false };
}
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
const roboticsMult = state.upgrades?.automated_robotics ? 2.0 : 1.0;
const baseCycleTime = Math.max(0.1, p.baseCycleTime / (milestoneMult * roboticsMult));
const isBottlenecked = !!pState.isBottlenecked;
// Output velocity: units per second
const prodRate = isBottlenecked ? 0 : (1 / baseCycleTime) * p.outputAmount * pState.lines;
// Logistics revenue rate: items sold passively per second * price
const logisticsUnits = pState.logistics || 0;
const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0;
const salePrice = p.basePrice * logisticsPriceMult;
const maxSellCapacity = logisticsUnits * 0.75;
// If stockpiled, sells at max fleet capacity; otherwise bottlenecked by production velocity
const effectiveSellRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : prodRate);
const revRate = effectiveSellRate * salePrice;
return { prodRate, revRate, isBottlenecked, milestoneMult };
}
/**
* Mounts the production floor DOM structure ONCE
*/
@ -165,7 +193,8 @@ export function mountProductionFloor(container, state, onAction, engine) {
}
const nextM = getNextMilestone(pState.lines);
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
const rates = getProductOutputRates(p, pState, state);
const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
const canAffordBuy = state.cash >= calc.cost;
const managerCost = p.baseManagerCost;
@ -231,12 +260,22 @@ export function mountProductionFloor(container, state, onAction, engine) {
<div class="cycle-bar-fill" id="progress-${p.id}" style="width: ${progressPct}%; background-color: ${p.color};"></div>
</div>
<!-- Stock and Inventory Summary -->
<!-- Stock, Velocity and Revenue Summary -->
<div class="facility-stats-grid">
<div class="stat-cell">
<span class="stat-label">Stockpile:</span>
<span class="stat-value font-mono"><strong id="stock-${p.id}">${fmtNum(pState.stock, 0)}</strong> ${p.unit}</span>
</div>
<div class="stat-cell">
<span class="stat-label">Output Velocity:</span>
<span class="stat-value font-mono ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}" id="rate-units-${p.id}">
${rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`}
</span>
</div>
<div class="stat-cell">
<span class="stat-label">Logistics Rev:</span>
<span class="stat-value font-mono text-profit" id="rate-rev-${p.id}">+$${fmtNum(rates.revRate, 2)}/s</span>
</div>
<div class="stat-cell">
<span class="stat-label">Wholesale Price:</span>
<span class="stat-value font-mono">${fmtCurrency(p.basePrice)}</span>
@ -245,6 +284,10 @@ export function mountProductionFloor(container, state, onAction, engine) {
<span class="stat-label">Logistics Fleet:</span>
<span class="stat-value font-mono" id="logistics-${p.id}">${pState.logistics || 0} fleets</span>
</div>
<div class="stat-cell">
<span class="stat-label">Milestone Eff:</span>
<span class="stat-value font-mono" id="eff-${p.id}">${milestoneMult}x Speed</span>
</div>
</div>
${inputsHtml}
@ -402,7 +445,40 @@ export function updateProductionFloor(container, state, engine) {
continue;
}
// Progress Bar
// Live Individual Factory Rates & Output
const rates = getProductOutputRates(p, pState, state);
const elRateUnits = document.getElementById(`rate-units-${p.id}`);
if (elRateUnits) {
const unitsText = rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`;
if (elRateUnits.textContent !== unitsText) {
elRateUnits.textContent = unitsText;
}
elRateUnits.className = `stat-value font-mono ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}`;
}
const elRateRev = document.getElementById(`rate-rev-${p.id}`);
if (elRateRev) {
const revText = `+$${fmtNum(rates.revRate, 2)}/s`;
if (elRateRev.textContent !== revText) {
elRateRev.textContent = revText;
}
}
const elEff = document.getElementById(`eff-${p.id}`);
if (elEff) {
const effText = `${rates.milestoneMult}x Speed`;
if (elEff.textContent !== effText) {
elEff.textContent = effText;
}
}
const elLogistics = document.getElementById(`logistics-${p.id}`);
if (elLogistics) {
const logText = `${pState.logistics || 0} fleets`;
if (elLogistics.textContent !== logText) {
elLogistics.textContent = logText;
}
}
const elProgress = document.getElementById(`progress-${p.id}`);
if (elProgress) {
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));