// 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 = `
`;
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 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}
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 = `