diff --git a/frontend/css/style.css b/frontend/css/style.css index b831cd5..05a50e1 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -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 */ diff --git a/frontend/js/ui/productionView.js b/frontend/js/ui/productionView.js index 0fd0544..0812d52 100644 --- a/frontend/js/ui/productionView.js +++ b/frontend/js/ui/productionView.js @@ -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) {
- +
Stockpile: ${fmtNum(pState.stock, 0)} ${p.unit}
+
+ Output Velocity: + + ${rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`} + +
+
+ Logistics Rev: + +$${fmtNum(rates.revRate, 2)}/s +
Wholesale Price: ${fmtCurrency(p.basePrice)} @@ -245,6 +284,10 @@ export function mountProductionFloor(container, state, onAction, engine) { Logistics Fleet: ${pState.logistics || 0} fleets
+
+ Milestone Eff: + ${milestoneMult}x Speed +
${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));