fix(ui): eliminate card jitter on input shortage and display individual factory output metrics
This commit is contained in:
parent
6a7aa072e4
commit
edc9a843b7
2 changed files with 123 additions and 7 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue