// Production - The Idle Game: Production Floor View // High-performance fine-grained DOM mounting & updating without innerHTML teardowns 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 }; } /** * Mounts the production floor DOM structure ONCE */ export function mountProductionFloor(container, state, onAction) { const selectedSector = state.selectedSector || 'all'; const buyMode = state.buyMode || '1'; const filteredProducts = PRODUCTS.filter(p => { if (selectedSector === 'all') return true; return p.sector === selectedSector; }); let html = `
${SECTORS.map(s => ` `).join('')}
`; 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 += `
${sector?.name || 'Industry'} • Tier ${p.tier}

${p.name}

${p.description}

`; continue; } const nextM = getNextMilestone(pState.lines); const 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 = `
Inputs per line:
${p.inputs.map(inp => { const inpProduct = getProduct(inp.id); const currentStock = state.products[inp.id]?.stock || 0; const neededPerLine = inp.amount * pState.lines; const isLow = currentStock < neededPerLine; return ` ${inpProduct?.name}: ${fmtNum(currentStock, 0)} / ${fmtNum(neededPerLine, 0)} `; }).join('')}
`; } 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 += `
${sector?.name} • Tier ${p.tier}
${statusBadge}

${p.name}

${p.facilityName} • ${pState.lines} lines (${nextM - pState.lines} to 2x)
Stockpile: ${fmtNum(pState.stock, 0)} ${p.unit}
Wholesale Price: ${fmtCurrency(p.basePrice)}
Logistics Fleet: ${pState.logistics || 0} fleets
${inputsHtml}
${!pState.manager ? ` ` : ` 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('.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) { const buyMode = state.buyMode || '1'; const cash = state.cash; for (const p of PRODUCTS) { const pState = state.products[p.id]; if (!pState) continue; // If card is locked: only update unlock button disabled state 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; } // 1. Progress Bar (smooth width update) const elProgress = document.getElementById(`progress-${p.id}`); if (elProgress) { const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100)); elProgress.style.width = `${progressPct}%`; } // 2. Stockpile Text const elStock = document.getElementById(`stock-${p.id}`); if (elStock) { const stockText = fmtNum(pState.stock, 0); if (elStock.textContent !== stockText) { elStock.textContent = stockText; } } // 3. Status Badge (only update if bottlenecked state changed) 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)`; } } // 4. 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); } } } // 5. 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; } } // 6. 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; } } // 7. 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; } } // 8. Selected Card class const elCard = document.getElementById(`card-${p.id}`); if (elCard) { const isSelected = state.selectedProductId === p.id; elCard.classList.toggle('selected', isSelected); } } }