// 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 calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode);
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)
`}
${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}.
${product.facilityName}
Architectural production plant schematic.
High-throughput architectural layout optimized for Tier ${product.tier} commercial processing.
Executive Economics & Production Telemetry
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)}
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
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)
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('')}
Control automated wholesale liquidation and raw material delivery permissions.
Fleet Capacity: ${pState.logistics || 0} fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)} u/s)
Liquidate stockpile instantly onto the open wholesale spot market.
Supply Chain Ecosystem
${(!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.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 calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode);
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}` : ''}`;
});
}