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);
|
box-shadow: var(--shadow-sm);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 410px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-card:hover {
|
.facility-card:hover {
|
||||||
|
|
@ -652,6 +655,8 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
min-height: 60px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-icon-wrap {
|
.facility-icon-wrap {
|
||||||
|
|
@ -671,20 +676,31 @@ body {
|
||||||
.facility-title-wrap {
|
.facility-title-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-meta-row {
|
.facility-meta-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 22px;
|
||||||
|
height: 22px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sector-badge {
|
.sector-badge {
|
||||||
font-size: 0.72rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-name {
|
.facility-name {
|
||||||
|
|
@ -692,11 +708,17 @@ body {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-type-sub {
|
.facility-type-sub {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facility-desc {
|
.facility-desc {
|
||||||
|
|
@ -714,6 +736,11 @@ body {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-success {
|
.badge-success {
|
||||||
|
|
@ -754,27 +781,40 @@ body {
|
||||||
/* Facility Stats Grid */
|
/* Facility Stats Grid */
|
||||||
.facility-stats-grid {
|
.facility-stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
background: var(--bg-surface-sub);
|
background: var(--bg-surface-sub);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
min-height: 84px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-cell {
|
.stat-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 0.68rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 0.8rem;
|
font-size: 0.78rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: var(--amber-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inputs Strip */
|
/* Inputs Strip */
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,34 @@ export function calculateMultiCost(baseCost, currentCount, multiplier) {
|
||||||
return { count: countToBuy, cost: totalCost };
|
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
|
* Mounts the production floor DOM structure ONCE
|
||||||
*/
|
*/
|
||||||
|
|
@ -165,7 +193,8 @@ export function mountProductionFloor(container, state, onAction, engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextM = getNextMilestone(pState.lines);
|
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 calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
|
||||||
const canAffordBuy = state.cash >= calc.cost;
|
const canAffordBuy = state.cash >= calc.cost;
|
||||||
const managerCost = p.baseManagerCost;
|
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 class="cycle-bar-fill" id="progress-${p.id}" style="width: ${progressPct}%; background-color: ${p.color};"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stock and Inventory Summary -->
|
<!-- Stock, Velocity and Revenue Summary -->
|
||||||
<div class="facility-stats-grid">
|
<div class="facility-stats-grid">
|
||||||
<div class="stat-cell">
|
<div class="stat-cell">
|
||||||
<span class="stat-label">Stockpile:</span>
|
<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>
|
<span class="stat-value font-mono"><strong id="stock-${p.id}">${fmtNum(pState.stock, 0)}</strong> ${p.unit}</span>
|
||||||
</div>
|
</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">
|
<div class="stat-cell">
|
||||||
<span class="stat-label">Wholesale Price:</span>
|
<span class="stat-label">Wholesale Price:</span>
|
||||||
<span class="stat-value font-mono">${fmtCurrency(p.basePrice)}</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-label">Logistics Fleet:</span>
|
||||||
<span class="stat-value font-mono" id="logistics-${p.id}">${pState.logistics || 0} fleets</span>
|
<span class="stat-value font-mono" id="logistics-${p.id}">${pState.logistics || 0} fleets</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
${inputsHtml}
|
${inputsHtml}
|
||||||
|
|
@ -402,7 +445,40 @@ export function updateProductionFloor(container, state, engine) {
|
||||||
continue;
|
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}`);
|
const elProgress = document.getElementById(`progress-${p.id}`);
|
||||||
if (elProgress) {
|
if (elProgress) {
|
||||||
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue