// Production - The Idle Game: Full Facility Intelligence Dossier (Unit Detail View) // Data-rich deep-dive view for individual facilities with dual photography, analytics, and strategic controls import { PRODUCTS, SECTORS, getProduct, getUnitMilestoneMultiplier, getNextMilestone } from '../data/products.js'; import { fmtCurrency, fmtNum, fmtRate } from '../core/formatters.js'; import { getProductOutputRates, computeProductNetFlow, calculateMultiCost } from './production/ratesCalculator.js'; let currentMountedDetailProductId = null; export function mountUnitDetailView(container, state, onAction, engine) { const selectedId = state.selectedProductId || 'wheat'; const product = getProduct(selectedId) || PRODUCTS[0]; const pState = state.products[product.id] || { stock: 0, lines: 0, logistics: 0, unlocked: false, productionTarget: 1.0, logisticsEnabled: true, supplyDownstreamEnabled: true, cycleProgress: 0 }; const sector = SECTORS.find(s => s.id === product.sector); currentMountedDetailProductId = product.id; // Rates and Net Flows const rates = getProductOutputRates(product, pState, state); const netFlow = computeProductNetFlow(product, state); const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines || 0); const nextM = getNextMilestone(pState.lines || 0); const buyMode = state.buyMode || '1'; const clientRep = state.repAllocations?.clients || 0; const lineDiscount = Math.min(0.50, 0.002 * clientRep); const calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount); const canAffordBuy = state.cash >= calc.cost; const canAffordManager = state.cash >= product.baseManagerCost; const logisticsCost = product.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0); const canAffordLogistics = state.cash >= logisticsCost; // Throttle levels const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0; const throttleLevels = [ { val: 1.0, label: '100%' }, { val: 0.75, label: '75%' }, { val: 0.50, label: '50%' }, { val: 0.25, label: '25%' }, { val: 0.0, label: '0% (Off)' } ]; // Downstream products that consume this product const downstream = PRODUCTS.filter(p => p.inputs && p.inputs.some(inp => inp.id === product.id)); // Liquidation Spot Values const totalLiquidationValue = (pState.stock || 0) * product.basePrice; const halfLiquidationValue = totalLiquidationValue * 0.5; // Production Speed & Volume Projections const cyclesPerSec = rates.cyclesPerSec || 0; const unitsPerSec = rates.prodRate || 0; const unitsPerMin = unitsPerSec * 60; const unitsPerHour = unitsPerMin * 60; const revPerSec = rates.revRate || 0; const revPerHour = revPerSec * 3600; // Valuation and Revenue Contribution const facilityValuation = (pState.lines || 0) * product.baseFacilityCost; const inventoryValuation = (pState.stock || 0) * product.basePrice; const totalCompanyCashRate = engine?.cashRate || 0; const companyRevShare = totalCompanyCashRate > 0.001 ? Math.min(100, Math.max(0, (rates.revRate / totalCompanyCashRate) * 100)) : 0; // Actual Cycle Time adjusted for milestone speed multiplier & throttle const effectiveCycleTime = product.baseCycleTime / (milestoneMult * Math.max(0.01, currentThrottle)); const effectiveLines = (pState.lines || 0) * currentThrottle; // Upstream Stock & Bottleneck Risk Assessment let bottleneckStatus = 'optimal'; let bottleneckLabel = 'Optimal Input Reserves'; if (product.inputs && product.inputs.length > 0) { for (const inp of product.inputs) { const parentState = state.products[inp.id]; const neededPerBatch = inp.amount * (pState.lines || 1); if (!parentState || (parentState.stock || 0) < 1) { bottleneckStatus = 'bottleneck'; bottleneckLabel = 'Starved: Zero Raw Materials'; break; } else if ((parentState.stock || 0) < neededPerBatch) { bottleneckStatus = 'low'; bottleneckLabel = 'Low Upstream Reserves'; } } } // Photographic Asset Paths const facilityImgPath = `assets/images/facilities/${product.id}_facility.jpg`; const productImgPath = `assets/images/products/${product.id}_product.jpg`; let html = `
${sector?.name} Tier ${product.tier} Industry ${bottleneckLabel}
${product.facilityName} • ${sector?.name}

${product.name}

${product.description}

${!pState.manager ? ` ` : ` Lead Active (Automated) `}
Primary Commodity Specimen ${product.unit}
${product.name}
${product.name} Archival material specimen registered in industrial catalog.
Market Spot Price: ${fmtCurrency(product.basePrice)}
Warehouse Stock: ${fmtNum(pState.stock || 0, 0)} ${product.unit}
Inventory Value: ${fmtCurrency(inventoryValuation)}
Commercial grade commodity wholesale specimen traded at ${fmtCurrency(product.basePrice)} per ${product.unit}.
Operating Facility Layout ${product.facilityName}
${product.facilityName}
${product.facilityName} Architectural production plant schematic.
High-throughput architectural layout optimized for Tier ${product.tier} commercial processing.
Executive Economics & Production Telemetry
Financial Performance & Share
Wholesale Unit Price: ${fmtCurrency(product.basePrice)}
Logistics Revenue Rate: +$${fmtNum(rates.revRate, 2)}/s
Projected Hourly Revenue: ${fmtCurrency(revPerHour)}/h
Enterprise Revenue Share: ${fmtNum(companyRevShare, 1)}%
Inventory Warehouse Value: ${fmtCurrency(inventoryValuation)}
Facility Plant Capital Value: ${fmtCurrency(facilityValuation)}
Industrial Dynamics & Velocity
Operating Industrial Lines: ${pState.lines} lines (${nextM - pState.lines} to 2x)
Effective Cycle Time: ${effectiveCycleTime.toFixed(2)}s (${product.baseCycleTime.toFixed(1)}s raw)
Milestone Speed Multiplier: ${milestoneMult}x Speed Boost
Batches Processed / Sec: ${fmtNum(cyclesPerSec, 2)}/s
Production Velocity: ${fmtNum(unitsPerSec, 1)} units/s
Projected Hourly Output: ${fmtNum(unitsPerHour, 0)} ${product.unit}/h
Supply Chain Flow Balance
Plant Production Rate: +${fmtNum(unitsPerSec, 1)}/s
Downstream Consumption Pull: -${fmtNum(netFlow.consumedRate, 1)}/s
Wholesale Logistics Sales: -${fmtNum(netFlow.soldRate, 1)}/s
Net Inventory Drift (Δ/s): ${netFlow.netRate > 0.01 ? '+' : ''}${fmtNum(netFlow.netRate, 1)}/s
Current Operating Throttle: ${Math.round(currentThrottle * 100)}% (${fmtNum(effectiveLines, 1)} lines active)
Facility Output Throttle

Scale operating speed and raw material consumption proportionally.

${throttleLevels.map(lvl => { const isMatch = Math.abs(currentThrottle - lvl.val) < 0.01; const offClass = lvl.val === 0.0 ? 'pill-off' : ''; return ` `; }).join('')}
Logistics Fleet & Downstream Supply

Control automated wholesale liquidation and raw material delivery permissions.

Fleet Capacity: ${pState.logistics || 0} fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)} u/s)
Spot Market Liquidation

Liquidate stockpile instantly onto the open wholesale spot market.

Supply Chain Ecosystem
Direct Input Dependencies (Upstream)
${(!product.inputs || product.inputs.length === 0) ? `
Primary Commodity: Direct extraction from natural deposits with no upstream dependencies.
` : `
${product.inputs.map(inp => { const parent = getProduct(inp.id); const parentState = state.products[inp.id] || { stock: 0 }; const neededPerBatch = inp.amount * (pState.lines || 1); const consumptionPerSec = inp.amount * (pState.lines || 1) * cyclesPerSec; const isLow = (parentState.stock || 0) < neededPerBatch; return `

${parent.name}

${inp.amount} per batch • Running at ${fmtNum(consumptionPerSec, 1)}/s
Stock: ${fmtNum(parentState.stock, 0)} ${parent.unit}
`; }).join('')}
`}
Downstream Consumers (Industrial Demand)
${downstream.length === 0 ? `
End-tier Finished Product: Distributed directly to global commercial markets.
` : `
${downstream.map(child => { const childState = state.products[child.id] || { unlocked: false, lines: 0 }; const inpMatch = child.inputs.find(i => i.id === product.id); const isUnlocked = childState.unlocked; return `

${child.name}

Requires ${inpMatch?.amount || 1} ${product.unit} per batch • Tier ${child.tier}
${isUnlocked ? `${childState.lines} lines active` : 'Locked Blueprint'}
`; }).join('')}
`}
`; container.innerHTML = html; // Attach event listeners const btnBack = container.querySelector('#btn-back-to-floor'); if (btnBack) { btnBack.onclick = () => onAction({ type: 'CLOSE_UNIT_DETAIL' }); } const btnCycle = container.querySelector('#detail-btn-cycle'); if (btnCycle) { btnCycle.onclick = () => onAction({ type: 'MANUAL_CYCLE', productId: product.id }); } const btnExpand = container.querySelector('#detail-btn-expand'); if (btnExpand) { btnExpand.onclick = () => onAction({ type: 'BUY_LINES', productId: product.id }); } const btnHire = container.querySelector('#detail-btn-hire'); if (btnHire) { btnHire.onclick = () => onAction({ type: 'HIRE_MANAGER', productId: product.id }); } const btnLogistics = container.querySelector('#detail-btn-logistics'); if (btnLogistics) { btnLogistics.onclick = () => onAction({ type: 'BUY_LOGISTICS', productId: product.id }); } const btnToggleLog = container.querySelector('#detail-toggle-logistics'); if (btnToggleLog) { btnToggleLog.onclick = () => onAction({ type: 'TOGGLE_LOGISTICS', productId: product.id }); } const btnToggleDown = container.querySelector('#detail-toggle-downstream'); if (btnToggleDown) { btnToggleDown.onclick = () => onAction({ type: 'TOGGLE_DOWNSTREAM_SUPPLY', productId: product.id }); } const btnLiqHalf = container.querySelector('#detail-btn-liq-half'); if (btnLiqHalf) { btnLiqHalf.onclick = () => onAction({ type: 'LIQUIDATE_STOCK', productId: product.id, ratio: 0.5 }); } const btnLiqAll = container.querySelector('#detail-btn-liq-all'); if (btnLiqAll) { btnLiqAll.onclick = () => onAction({ type: 'LIQUIDATE_STOCK', productId: product.id, ratio: 1.0 }); } container.querySelectorAll('.btn-throttle-pill').forEach(btn => { btn.onclick = () => onAction({ type: 'SET_PRODUCTION_TARGET', productId: btn.dataset.product, target: parseFloat(btn.dataset.target) }); }); container.querySelectorAll('.btn-jump-unit').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); onAction({ type: 'OPEN_UNIT_DETAIL', productId: btn.dataset.product }); }; }); } /** * In-place update of numerical telemetry on the Unit Detail page without innerHTML teardowns */ export function updateUnitDetailView(container, state, engine) { const selectedId = state.selectedProductId || 'wheat'; if (selectedId !== currentMountedDetailProductId) { return; // Caller should remount if product changed } const product = getProduct(selectedId); if (!product) return; const pState = state.products[product.id] || { stock: 0, lines: 0, logistics: 0 }; const rates = getProductOutputRates(product, pState, state); const netFlow = computeProductNetFlow(product, state); const cash = state.cash; // Update real-time metric values const elRevRate = document.getElementById('det-rev-rate'); if (elRevRate) { elRevRate.textContent = `+$${fmtNum(rates.revRate, 2)}/s`; } const elRevHour = document.getElementById('det-rev-hour'); if (elRevHour) { elRevHour.textContent = `${fmtCurrency(rates.revRate * 3600)}/h`; } const elStockVal = document.getElementById('det-stock-val'); if (elStockVal) { elStockVal.textContent = fmtCurrency((pState.stock || 0) * product.basePrice); } const elCycleRate = document.getElementById('det-cycle-rate'); if (elCycleRate) { elCycleRate.textContent = `${fmtNum(rates.cyclesPerSec || 0, 2)}/s`; } const elProdRate = document.getElementById('det-prod-rate'); if (elProdRate) { elProdRate.textContent = `${fmtNum(rates.prodRate || 0, 1)} units/s`; } const elProdHour = document.getElementById('det-prod-hour'); if (elProdHour) { elProdHour.textContent = `${fmtNum((rates.prodRate || 0) * 3600, 0)} ${product.unit}/h`; } const elFlowProd = document.getElementById('det-flow-prod'); if (elFlowProd) { elFlowProd.textContent = `+${fmtNum(rates.prodRate || 0, 1)}/s`; } const elFlowConsumed = document.getElementById('det-flow-consumed'); if (elFlowConsumed) { elFlowConsumed.textContent = `-${fmtNum(netFlow.consumedRate || 0, 1)}/s`; } const elFlowSold = document.getElementById('det-flow-sold'); if (elFlowSold) { elFlowSold.textContent = `-${fmtNum(netFlow.soldRate || 0, 1)}/s`; } const elFlowNet = document.getElementById('det-flow-net'); if (elFlowNet) { const sign = netFlow.netRate > 0.01 ? '+' : ''; elFlowNet.textContent = `${sign}${fmtNum(netFlow.netRate || 0, 1)}/s`; elFlowNet.className = `m-val font-mono ${netFlow.netRate < -0.05 ? 'text-warning' : 'text-profit'}`; } // Hero Product Stock and Value const elHeroStock = document.getElementById('det-hero-stock'); if (elHeroStock) { elHeroStock.textContent = `${fmtNum(pState.stock || 0, 0)} ${product.unit}`; } const elHeroStockVal = document.getElementById('det-hero-stock-val'); if (elHeroStockVal) { elHeroStockVal.textContent = fmtCurrency((pState.stock || 0) * product.basePrice); } // Enterprise Revenue Share const totalCompanyCashRate = engine?.cashRate || 0; const companyRevShare = totalCompanyCashRate > 0.001 ? Math.min(100, Math.max(0, (rates.revRate / totalCompanyCashRate) * 100)) : 0; const elRevShare = document.getElementById('det-rev-share'); if (elRevShare) { elRevShare.textContent = `${fmtNum(companyRevShare, 1)}%`; } // Bottleneck & Raw Material Reserves Assessment let bottleneckStatus = 'optimal'; let bottleneckLabel = 'Optimal Input Reserves'; if (product.inputs && product.inputs.length > 0) { for (const inp of product.inputs) { const parentState = state.products[inp.id]; const neededPerBatch = inp.amount * (pState.lines || 1); if (!parentState || (parentState.stock || 0) < 1) { bottleneckStatus = 'bottleneck'; bottleneckLabel = 'Starved: Zero Raw Materials'; break; } else if ((parentState.stock || 0) < neededPerBatch) { bottleneckStatus = 'low'; bottleneckLabel = 'Low Upstream Reserves'; } } } const elBottleneckBadge = document.getElementById('det-bottleneck-badge'); const elBottleneckText = document.getElementById('det-bottleneck-text'); if (elBottleneckBadge && elBottleneckText) { elBottleneckBadge.className = `risk-badge risk-${bottleneckStatus}`; elBottleneckText.textContent = bottleneckLabel; } // Liquidation Spot Values const totalLiq = (pState.stock || 0) * product.basePrice; const elLiqAll = document.getElementById('det-liq-all-val'); if (elLiqAll) elLiqAll.textContent = fmtCurrency(totalLiq); const elLiqHalf = document.getElementById('det-liq-half-val'); if (elLiqHalf) elLiqHalf.textContent = fmtCurrency(totalLiq * 0.5); const btnLiqHalf = document.getElementById('detail-btn-liq-half'); if (btnLiqHalf) btnLiqHalf.disabled = (pState.stock || 0) < 2; const btnLiqAll = document.getElementById('detail-btn-liq-all'); if (btnLiqAll) btnLiqAll.disabled = (pState.stock || 0) < 1; // Expand Button disabled state const buyMode = state.buyMode || '1'; const clientRep = state.repAllocations?.clients || 0; const lineDiscount = Math.min(0.50, 0.002 * clientRep); const calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount); const btnExpand = document.getElementById('detail-btn-expand'); if (btnExpand) { btnExpand.disabled = cash < calc.cost; const elExpText = document.getElementById('detail-expand-text'); if (elExpText) elExpText.textContent = `+${calc.count} Lines (${fmtCurrency(calc.cost)})`; } // Manager Button disabled state const btnHire = document.getElementById('detail-btn-hire'); if (btnHire) { btnHire.disabled = cash < product.baseManagerCost; } // Logistics Fleet Button const logisticsCost = product.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0); const btnLogistics = document.getElementById('detail-btn-logistics'); if (btnLogistics) { btnLogistics.disabled = cash < logisticsCost; } // Throttle buttons active state const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0; container.querySelectorAll('.btn-throttle-pill').forEach(btn => { const btnVal = parseFloat(btn.dataset.target); const isMatch = Math.abs(currentThrottle - btnVal) < 0.01; const offClass = btnVal === 0.0 ? 'pill-off' : ''; btn.className = `btn-throttle-pill ${isMatch ? `active ${offClass}` : ''}`; }); }