// Production - The Idle Game: Production Floor View
// High-performance fine-grained DOM mounting & updating with comprehensive sector output metrics
import { PRODUCTS, SECTORS, getProduct, getNextMilestone, getUnitMilestoneMultiplier } from '../data/products.js';
import { fmtCurrency, fmtNum } from '../core/formatters.js';
export function calculateMultiCost(baseCost, currentCount, multiplier) {
const growth = 1.15;
let countToBuy = 1;
if (multiplier === '1') countToBuy = 1;
else if (multiplier === '10') countToBuy = 10;
else if (multiplier === '100') countToBuy = 100;
else if (multiplier === 'next') {
const nextTarget = getNextMilestone(currentCount);
countToBuy = Math.max(1, nextTarget - currentCount);
} else if (multiplier === 'max') {
countToBuy = 1;
}
let totalCost = 0;
for (let i = 0; i < countToBuy; i++) {
totalCost += baseCost * Math.pow(growth, currentCount + i);
}
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
*/
export function mountProductionFloor(container, state, onAction, engine) {
const selectedSector = state.selectedSector || 'all';
const buyMode = state.buyMode || '1';
const currentSectorObj = SECTORS.find(s => s.id === selectedSector);
const filteredProducts = PRODUCTS.filter(p => {
if (selectedSector === 'all') return true;
return p.sector === selectedSector;
});
let html = `
`;
// 1. If a specific sector is selected: Render the Sector Executive Summary Banner
if (selectedSector !== 'all' && currentSectorObj) {
const stats = engine?.sectorStats?.[selectedSector] || { revenueRate: 0, unitsRate: 0, activeLines: 0, totalProduced: 0 };
html += `
INDUSTRY OUTPUT OVERVIEW
${currentSectorObj.name}
${currentSectorObj.description}
Wholesale Output Rate:
+$${fmtNum(stats.revenueRate, 2)}/s
Production Velocity:
${fmtNum(stats.unitsRate, 1)} units/s
Operating Lines:
${stats.activeLines} active
Lifetime Sector Output:
${fmtNum(stats.totalProduced, 0)} units
`;
}
// 2. If 'All Sectors' is selected: Render the Industry Output Grid summarizing all sectors
if (selectedSector === 'all') {
html += `
${SECTORS.map(s => {
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0, unitsRate: 0, activeLines: 0 };
return `
Output Rate
+$${fmtNum(stats.revenueRate, 2)}/s
Velocity
${fmtNum(stats.unitsRate, 1)} u/s
`;
}).join('')}
`;
}
html += ``;
for (const p of filteredProducts) {
const pState = state.products[p.id] || { unlocked: false, lines: 0, stock: 0, cycleProgress: 0, isBottlenecked: false };
const sector = SECTORS.find(s => s.id === p.sector);
const isSelected = state.selectedProductId === p.id;
if (!pState.unlocked) {
const canAffordUnlock = state.cash >= p.unlockCost;
html += `
`;
continue;
}
const nextM = getNextMilestone(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;
const canAffordManager = state.cash >= managerCost;
const logisticsCost = p.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0);
const canAffordLogistics = state.cash >= logisticsCost;
const isRaw = !p.inputs || p.inputs.length === 0;
let inputsHtml = '';
if (!isRaw) {
inputsHtml = `
`;
}
let statusBadge = '';
if (pState.isBottlenecked) {
statusBadge = `
Waiting for Inputs`;
} else if (pState.lines > 0) {
statusBadge = `
Operating (${milestoneMult}x Speed)`;
} else {
statusBadge = `
Idle (0 Lines)`;
}
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
html += `
Stockpile:
${fmtNum(pState.stock, 0)} ${p.unit}
Production Output:
${rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`}
Logistics Rev:
+$${fmtNum(rates.revRate, 2)}/s
Wholesale Price:
${fmtCurrency(p.basePrice)}
Logistics Auto-Sell:
${pState.logistics || 0} fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)}/s)
Milestone Eff:
${milestoneMult}x Speed
${inputsHtml}
${!pState.manager ? `
` : `
Lead Active (Auto)
`}
`;
}
html += `
`;
container.innerHTML = html;
// Attach event listeners ONCE
container.querySelectorAll('.sector-pill').forEach(btn => {
btn.onclick = () => onAction({ type: 'SELECT_SECTOR', sector: btn.dataset.sector });
});
container.querySelectorAll('.sector-mini-card').forEach(card => {
card.onclick = () => onAction({ type: 'SELECT_SECTOR', sector: card.dataset.sector });
});
container.querySelectorAll('.facility-card').forEach(card => {
card.onclick = (e) => {
if (e.target.closest('button')) return;
onAction({ type: 'SELECT_PRODUCT', productId: card.dataset.product });
};
});
container.querySelectorAll('.btn-unlock').forEach(btn => {
btn.onclick = () => onAction({ type: 'UNLOCK_PRODUCT', productId: btn.dataset.product });
});
container.querySelectorAll('.btn-cycle').forEach(btn => {
btn.onclick = () => onAction({ type: 'MANUAL_CYCLE', productId: btn.dataset.product });
});
container.querySelectorAll('.btn-expand').forEach(btn => {
btn.onclick = () => onAction({ type: 'BUY_LINES', productId: btn.dataset.product });
});
container.querySelectorAll('.btn-hire-mgr').forEach(btn => {
btn.onclick = () => onAction({ type: 'HIRE_MANAGER', productId: btn.dataset.product });
});
container.querySelectorAll('.btn-expand-logistics').forEach(btn => {
btn.onclick = () => onAction({ type: 'BUY_LOGISTICS', productId: btn.dataset.product });
});
}
/**
* Fine-grained in-place DOM updates (0% layout thrashing, 0% DOM teardown)
*/
export function updateProductionFloor(container, state, engine) {
const buyMode = state.buyMode || '1';
const cash = state.cash;
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, 1)}/s`;
if (elPillRate.textContent !== text) elPillRate.textContent = text;
}
// Update All-Sectors Mini Cards if on 'all'
if (selectedSector === 'all') {
const elMiniRev = document.getElementById(`all-sec-rev-${s.id}`);
if (elMiniRev) {
const text = `+$${fmtNum(stats.revenueRate, 2)}/s`;
if (elMiniRev.textContent !== text) elMiniRev.textContent = text;
}
const elMiniUnits = document.getElementById(`all-sec-units-${s.id}`);
if (elMiniUnits) {
const text = `${fmtNum(stats.unitsRate, 1)} u/s`;
if (elMiniUnits.textContent !== text) elMiniUnits.textContent = text;
}
const elMiniLines = document.getElementById(`all-sec-lines-${s.id}`);
if (elMiniLines) {
const text = `${stats.activeLines} lines active`;
if (elMiniLines.textContent !== text) elMiniLines.textContent = text;
}
}
}
}
// 2. Update Sector Summary Banner if viewing a single sector
if (selectedSector !== 'all') {
const stats = engine?.sectorStats?.[selectedSector];
if (stats) {
const elRev = document.getElementById('sec-sum-rev');
if (elRev) {
const text = `+$${fmtNum(stats.revenueRate, 2)}/s`;
if (elRev.textContent !== text) elRev.textContent = text;
}
const elUnits = document.getElementById('sec-sum-units');
if (elUnits) {
const text = `${fmtNum(stats.unitsRate, 1)} units/s`;
if (elUnits.textContent !== text) elUnits.textContent = text;
}
const elLines = document.getElementById('sec-sum-lines');
if (elLines) {
const text = `${stats.activeLines} active`;
if (elLines.textContent !== text) elLines.textContent = text;
}
const elTotal = document.getElementById('sec-sum-total');
if (elTotal) {
const text = `${fmtNum(stats.totalProduced, 0)} units`;
if (elTotal.textContent !== text) elTotal.textContent = text;
}
}
}
// 3. Update Facility Cards
for (const p of PRODUCTS) {
const pState = state.products[p.id];
if (!pState) continue;
if (!pState.unlocked) {
const btnUnlock = document.getElementById(`btn-unlock-${p.id}`);
if (btnUnlock) {
const canAfford = cash >= p.unlockCost;
if (btnUnlock.disabled !== !canAfford) {
btnUnlock.disabled = !canAfford;
}
}
continue;
}
// 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 count = pState.logistics || 0;
const logText = `${count} fleets (${fmtNum(count * 0.75, 2)}/s)`;
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));
elProgress.style.width = `${progressPct}%`;
}
// Stockpile Text
const elStock = document.getElementById(`stock-${p.id}`);
if (elStock) {
const stockText = fmtNum(pState.stock, 0);
if (elStock.textContent !== stockText) {
elStock.textContent = stockText;
}
}
// Status Badge
const elBadge = document.getElementById(`badge-${p.id}`);
if (elBadge) {
const isBottlenecked = pState.isBottlenecked;
const hasWarning = elBadge.classList.contains('badge-warning');
if (isBottlenecked && !hasWarning) {
elBadge.className = 'status-badge badge-warning';
elBadge.innerHTML = ` Waiting for Inputs`;
} else if (!isBottlenecked && hasWarning) {
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
elBadge.className = 'status-badge badge-success';
elBadge.innerHTML = ` Operating (${milestoneMult}x Speed)`;
}
}
// Input Chips Stock
if (p.inputs && p.inputs.length > 0) {
for (const inp of p.inputs) {
const currentStock = state.products[inp.id]?.stock || 0;
const elChipVal = document.getElementById(`chip-val-${p.id}-${inp.id}`);
if (elChipVal) {
const textVal = fmtNum(currentStock, 0);
if (elChipVal.textContent !== textVal) {
elChipVal.textContent = textVal;
}
}
const elChip = document.getElementById(`chip-${p.id}-${inp.id}`);
if (elChip) {
const needed = inp.amount * pState.lines;
const isLow = currentStock < needed;
elChip.classList.toggle('low-stock', isLow);
}
}
}
// Expand Button disabled state
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
const elBtnExpand = document.getElementById(`btn-expand-${p.id}`);
if (elBtnExpand) {
const canAfford = cash >= calc.cost;
if (elBtnExpand.disabled !== !canAfford) {
elBtnExpand.disabled = !canAfford;
}
}
// Hire Manager disabled state
const elBtnHire = document.getElementById(`btn-hire-${p.id}`);
if (elBtnHire) {
const canAfford = cash >= p.baseManagerCost;
if (elBtnHire.disabled !== !canAfford) {
elBtnHire.disabled = !canAfford;
}
}
// Logistics Button disabled state
const logisticsCost = p.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0);
const elBtnLogistics = document.getElementById(`btn-logistics-${p.id}`);
if (elBtnLogistics) {
const canAfford = cash >= logisticsCost;
if (elBtnLogistics.disabled !== !canAfford) {
elBtnLogistics.disabled = !canAfford;
}
const elBtnLogisticsText = document.getElementById(`btn-logistics-text-${p.id}`);
if (elBtnLogisticsText) {
const text = `+Fleet (${fmtCurrency(logisticsCost)})`;
if (elBtnLogisticsText.textContent !== text) {
elBtnLogisticsText.textContent = text;
}
}
}
// Selected Card class
const elCard = document.getElementById(`card-${p.id}`);
if (elCard) {
const isSelected = state.selectedProductId === p.id;
elCard.classList.toggle('selected', isSelected);
}
}
}