581 lines
28 KiB
JavaScript
581 lines
28 KiB
JavaScript
// 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 = `
|
|
<div class="production-toolbar">
|
|
<div class="sector-tabs-scroll">
|
|
<button class="sector-pill ${selectedSector === 'all' ? 'active' : ''}" data-sector="all" title="All Sectors">
|
|
<iconify-icon icon="lucide:layout-grid"></iconify-icon>
|
|
<span class="sector-pill-rate font-mono" id="pill-rate-all">+$${fmtNum(engine?.cashRate || 0, 1)}/s</span>
|
|
</button>
|
|
${SECTORS.map(s => {
|
|
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 };
|
|
return `
|
|
<button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}" title="${s.name}">
|
|
<iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon>
|
|
<span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 1)}/s</span>
|
|
</button>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 += `
|
|
<div class="sector-summary-banner" style="border-left: 4px solid ${currentSectorObj.color};">
|
|
<div class="sector-summary-main">
|
|
<div class="sector-summary-icon" style="background: ${currentSectorObj.color}15; color: ${currentSectorObj.color};">
|
|
<iconify-icon icon="${currentSectorObj.icon}" class="icon-lg"></iconify-icon>
|
|
</div>
|
|
<div>
|
|
<div class="sector-summary-badge" style="color: ${currentSectorObj.color};">INDUSTRY OUTPUT OVERVIEW</div>
|
|
<h2 class="sector-summary-title">${currentSectorObj.name}</h2>
|
|
<p class="sector-summary-desc">${currentSectorObj.description}</p>
|
|
</div>
|
|
</div>
|
|
<div class="sector-metrics-strip">
|
|
<div class="sector-metric-cell">
|
|
<span class="sec-metric-label">Wholesale Output Rate:</span>
|
|
<strong class="sec-metric-val font-mono text-profit" id="sec-sum-rev">+$${fmtNum(stats.revenueRate, 2)}/s</strong>
|
|
</div>
|
|
<div class="sector-metric-cell">
|
|
<span class="sec-metric-label">Production Velocity:</span>
|
|
<strong class="sec-metric-val font-mono" id="sec-sum-units">${fmtNum(stats.unitsRate, 1)} units/s</strong>
|
|
</div>
|
|
<div class="sector-metric-cell">
|
|
<span class="sec-metric-label">Operating Lines:</span>
|
|
<strong class="sec-metric-val font-mono" id="sec-sum-lines">${stats.activeLines} active</strong>
|
|
</div>
|
|
<div class="sector-metric-cell">
|
|
<span class="sec-metric-label">Lifetime Sector Output:</span>
|
|
<strong class="sec-metric-val font-mono" id="sec-sum-total">${fmtNum(stats.totalProduced, 0)} units</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 2. If 'All Sectors' is selected: Render the Industry Output Grid summarizing all sectors
|
|
if (selectedSector === 'all') {
|
|
html += `
|
|
<div class="all-sectors-summary-grid">
|
|
${SECTORS.map(s => {
|
|
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0, unitsRate: 0, activeLines: 0 };
|
|
return `
|
|
<div class="sector-mini-card" data-sector="${s.id}" style="border-top: 3px solid ${s.color};">
|
|
<div class="mini-card-header">
|
|
<div class="mini-card-icon" style="background: ${s.color}15; color: ${s.color};">
|
|
<iconify-icon icon="${s.icon}"></iconify-icon>
|
|
</div>
|
|
<div class="mini-card-title-wrap">
|
|
<h4 class="mini-card-name">${s.name}</h4>
|
|
<span class="mini-card-sub" id="all-sec-lines-${s.id}">${stats.activeLines} lines active</span>
|
|
</div>
|
|
</div>
|
|
<div class="mini-card-stats">
|
|
<div class="mini-stat-cell">
|
|
<span class="mini-stat-label">Output Rate</span>
|
|
<strong class="font-mono text-profit" id="all-sec-rev-${s.id}">+$${fmtNum(stats.revenueRate, 2)}/s</strong>
|
|
</div>
|
|
<div class="mini-stat-cell">
|
|
<span class="mini-stat-label">Velocity</span>
|
|
<strong class="font-mono" id="all-sec-units-${s.id}">${fmtNum(stats.unitsRate, 1)} u/s</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += `<div class="production-grid">`;
|
|
|
|
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 += `
|
|
<div class="facility-card locked-card ${isSelected ? 'selected' : ''}" id="card-${p.id}" data-product="${p.id}">
|
|
<div class="facility-header">
|
|
<div class="facility-icon-wrap" style="background: ${p.color}15; color: ${p.color};">
|
|
<iconify-icon icon="${p.icon}" class="icon-lg"></iconify-icon>
|
|
</div>
|
|
<div class="facility-title-wrap">
|
|
<span class="sector-badge" style="color: ${sector?.color || '#64748b'};">
|
|
${sector?.name || 'Industry'} • Tier ${p.tier}
|
|
</span>
|
|
<h3 class="facility-name">${p.name}</h3>
|
|
</div>
|
|
</div>
|
|
<p class="facility-desc">${p.description}</p>
|
|
<div class="locked-footer">
|
|
<button class="btn-primary-corp btn-unlock" id="btn-unlock-${p.id}" data-product="${p.id}" ${canAffordUnlock ? '' : 'disabled'}>
|
|
<iconify-icon icon="lucide:lock-open"></iconify-icon>
|
|
<span>Acquire Permit (${fmtCurrency(p.unlockCost)})</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="inputs-strip">
|
|
<span class="inputs-label">Inputs per line:</span>
|
|
<div class="inputs-chips">
|
|
${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 `
|
|
<span class="input-chip ${isLow ? 'low-stock' : ''}" id="chip-${p.id}-${inp.id}" title="${inpProduct?.name}">
|
|
<iconify-icon icon="${inpProduct?.icon || 'lucide:box'}"></iconify-icon>
|
|
${inpProduct?.name}: <strong id="chip-val-${p.id}-${inp.id}">${fmtNum(currentStock, 0)}</strong> / ${fmtNum(neededPerLine, 0)}
|
|
</span>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
let statusBadge = '';
|
|
if (pState.isBottlenecked) {
|
|
statusBadge = `<span class="status-badge badge-warning" id="badge-${p.id}"><iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs</span>`;
|
|
} else if (pState.lines > 0) {
|
|
statusBadge = `<span class="status-badge badge-success" id="badge-${p.id}"><iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)</span>`;
|
|
} else {
|
|
statusBadge = `<span class="status-badge badge-idle" id="badge-${p.id}">Idle (0 Lines)</span>`;
|
|
}
|
|
|
|
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
|
|
|
html += `
|
|
<div class="facility-card active-card ${isSelected ? 'selected' : ''}" id="card-${p.id}" data-product="${p.id}">
|
|
<div class="facility-header">
|
|
<div class="facility-icon-wrap" style="background: ${p.color}15; color: ${p.color};">
|
|
<iconify-icon icon="${p.icon}" class="icon-lg"></iconify-icon>
|
|
</div>
|
|
<div class="facility-title-wrap">
|
|
<div class="facility-meta-row">
|
|
<span class="sector-badge" style="color: ${sector?.color || '#64748b'};">
|
|
${sector?.name} • Tier ${p.tier}
|
|
</span>
|
|
<div id="badge-wrap-${p.id}">${statusBadge}</div>
|
|
</div>
|
|
<h3 class="facility-name">${p.name}</h3>
|
|
<span class="facility-type-sub" id="type-sub-${p.id}">${p.facilityName} • <strong>${pState.lines}</strong> lines (${nextM - pState.lines} to 2x)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cycle Progress Bar -->
|
|
<div class="cycle-bar-track">
|
|
<div class="cycle-bar-fill" id="progress-${p.id}" style="width: ${progressPct}%; background-color: ${p.color};"></div>
|
|
</div>
|
|
|
|
<!-- Stock, Velocity and Revenue Summary -->
|
|
<div class="facility-stats-grid">
|
|
<div class="stat-cell">
|
|
<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>
|
|
</div>
|
|
<div class="stat-cell">
|
|
<span class="stat-label">Production Output:</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">
|
|
<span class="stat-label">Wholesale Price:</span>
|
|
<span class="stat-value font-mono">${fmtCurrency(p.basePrice)}</span>
|
|
</div>
|
|
<div class="stat-cell" title="Logistics fleets automatically sell stockpiled goods continuously onto the wholesale market (0.75 units/sec per fleet)">
|
|
<span class="stat-label">Logistics Auto-Sell:</span>
|
|
<span class="stat-value font-mono" id="logistics-${p.id}">${pState.logistics || 0} fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)}/s)</span>
|
|
</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>
|
|
|
|
${inputsHtml}
|
|
|
|
<!-- Facility Actions Bar -->
|
|
<div class="facility-actions-row">
|
|
<button class="btn-corp-action btn-cycle" data-product="${p.id}" title="Run 1 manual production cycle">
|
|
<iconify-icon icon="lucide:play"></iconify-icon>
|
|
<span>Run Batch</span>
|
|
</button>
|
|
|
|
<button class="btn-primary-corp btn-expand" id="btn-expand-${p.id}" data-product="${p.id}" ${canAffordBuy ? '' : 'disabled'} title="Add more production lines to increase output volume">
|
|
<iconify-icon icon="lucide:plus-circle"></iconify-icon>
|
|
<span id="btn-expand-text-${p.id}">+${calc.count} Lines (${fmtCurrency(calc.cost)})</span>
|
|
</button>
|
|
|
|
${!pState.manager ? `
|
|
<button class="btn-corp-secondary btn-hire-mgr" id="btn-hire-${p.id}" data-product="${p.id}" ${canAffordManager ? '' : 'disabled'} title="Hires a Facility Lead who automatically runs production continuously without clicking">
|
|
<iconify-icon icon="lucide:user-check"></iconify-icon>
|
|
<span>Hire Lead (${fmtCurrency(managerCost)})</span>
|
|
</button>
|
|
` : `
|
|
<span class="mgr-hired-badge" title="Facility Lead hired: 100% automated continuous production active">
|
|
<iconify-icon icon="lucide:bot"></iconify-icon> <span>Lead Active (Auto)</span>
|
|
</span>
|
|
`}
|
|
|
|
<button class="btn-corp-secondary btn-expand-logistics" id="btn-logistics-${p.id}" data-product="${p.id}" ${canAffordLogistics ? '' : 'disabled'} title="Deploys an automated distribution fleet (+0.75 units/sec passive wholesale sales)">
|
|
<iconify-icon icon="lucide:truck"></iconify-icon>
|
|
<span id="btn-logistics-text-${p.id}">+Fleet (${fmtCurrency(logisticsCost)})</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
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 = `<iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs`;
|
|
} else if (!isBottlenecked && hasWarning) {
|
|
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
|
|
elBadge.className = 'status-badge badge-success';
|
|
elBadge.innerHTML = `<iconify-icon icon="lucide:check-circle"></iconify-icon> 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);
|
|
}
|
|
}
|
|
}
|