ProductionGame/frontend/js/ui/unitDetailView.js

702 lines
37 KiB
JavaScript

// 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 = `
<div class="unit-detail-canvas">
<!-- Navigation Breadcrumb & Header Controls (Clean in-universe badges, NO SYS-ID) -->
<div class="dossier-top-bar">
<button class="btn-dossier-back" id="btn-back-to-floor" title="Return to Production Floor (Esc)">
<iconify-icon icon="lucide:arrow-left"></iconify-icon>
<span>Return to Production Floor</span>
</button>
<div class="dossier-top-meta">
<span class="sector-tag" style="background: ${product.color}15; color: ${product.color};">
<iconify-icon icon="${product.icon}"></iconify-icon> ${sector?.name}
</span>
<span class="tier-tag">Tier ${product.tier} Industry</span>
<span class="risk-badge risk-${bottleneckStatus}" id="det-bottleneck-badge">
<iconify-icon icon="${bottleneckStatus === 'optimal' ? 'lucide:shield-check' : 'lucide:alert-triangle'}"></iconify-icon>
<span id="det-bottleneck-text">${bottleneckLabel}</span>
</span>
</div>
</div>
<!-- Unit Dossier Hero Banner -->
<div class="dossier-hero-card" style="border-top: 4px solid ${product.color};">
<div class="dossier-hero-main">
<div class="dossier-hero-icon-wrap" style="background: ${product.color}15; color: ${product.color};">
<iconify-icon icon="${product.icon}" class="icon-hero"></iconify-icon>
</div>
<div class="dossier-hero-text">
<div class="dossier-pretitle">${product.facilityName} &bull; ${sector?.name}</div>
<h1 class="dossier-title">${product.name}</h1>
<p class="dossier-desc">${product.description}</p>
</div>
</div>
<div class="dossier-hero-actions">
<button class="btn-primary-corp btn-dossier-expand" id="detail-btn-expand" ${canAffordBuy ? '' : 'disabled'}>
<iconify-icon icon="lucide:plus-circle"></iconify-icon>
<span id="detail-expand-text">+${calc.count} Lines (${fmtCurrency(calc.cost)})</span>
</button>
${!pState.manager ? `
<button class="btn-corp-secondary" id="detail-btn-hire" ${canAffordManager ? '' : 'disabled'} title="Hire Facility Lead to automate production">
<iconify-icon icon="lucide:user-check"></iconify-icon>
<span>Hire Lead (${fmtCurrency(product.baseManagerCost)})</span>
</button>
` : `
<span class="mgr-hired-badge">
<iconify-icon icon="lucide:bot"></iconify-icon> <span>Lead Active (Automated)</span>
</span>
`}
<button class="btn-corp-action" id="detail-btn-cycle" title="Run manual batch">
<iconify-icon icon="lucide:play"></iconify-icon>
<span>Run Batch</span>
</button>
</div>
</div>
<!-- Visual Showcase: Product Primacy (Hero Showcase) + Architectural Facility Reference (Clean Placeholders, NO file paths) -->
<div class="dossier-photo-grid">
<!-- 1. Primary Commodity Specimen Showpiece -->
<div class="dossier-photo-card hero-product-card">
<div class="photo-card-header">
<span class="photo-tag"><iconify-icon icon="lucide:box"></iconify-icon> Primary Commodity Specimen</span>
<span class="photo-spec font-mono">${product.unit}</span>
</div>
<div class="photo-wrapper">
<img src="${productImgPath}" alt="${product.name}" class="dossier-photo-img" onerror="this.onerror=null; this.parentElement.classList.add('photo-fallback'); this.style.display='none';">
<div class="photo-placeholder-box">
<iconify-icon icon="${product.icon}" class="photo-fallback-icon" style="color: ${product.color};"></iconify-icon>
<span class="photo-fallback-title">${product.name}</span>
<span class="photo-fallback-sub">Archival material specimen registered in industrial catalog.</span>
</div>
</div>
<div class="product-quick-stats">
<div class="p-stat-item">
<span class="p-stat-label">Market Spot Price:</span>
<span class="p-stat-val font-mono">${fmtCurrency(product.basePrice)}</span>
</div>
<div class="p-stat-item">
<span class="p-stat-label">Warehouse Stock:</span>
<span class="p-stat-val font-mono" id="det-hero-stock">${fmtNum(pState.stock || 0, 0)} ${product.unit}</span>
</div>
<div class="p-stat-item">
<span class="p-stat-label">Inventory Value:</span>
<span class="p-stat-val font-mono text-profit" id="det-hero-stock-val">${fmtCurrency(inventoryValuation)}</span>
</div>
</div>
<div class="photo-caption">
<span>Commercial grade commodity wholesale specimen traded at ${fmtCurrency(product.basePrice)} per ${product.unit}.</span>
</div>
</div>
<!-- 2. Facility Architecture Reference Card -->
<div class="dossier-photo-card">
<div class="photo-card-header">
<span class="photo-tag"><iconify-icon icon="lucide:building-2"></iconify-icon> Operating Facility Layout</span>
<span class="photo-spec font-mono">${product.facilityName}</span>
</div>
<div class="photo-wrapper secondary-wrapper">
<img src="${facilityImgPath}" alt="${product.facilityName}" class="dossier-photo-img" onerror="this.onerror=null; this.parentElement.classList.add('photo-fallback'); this.style.display='none';">
<div class="photo-placeholder-box">
<iconify-icon icon="${sector?.icon || 'lucide:factory'}" class="photo-fallback-icon" style="color: ${product.color};"></iconify-icon>
<span class="photo-fallback-title">${product.facilityName}</span>
<span class="photo-fallback-sub">Architectural production plant schematic.</span>
</div>
</div>
<div class="photo-caption secondary-caption">
<span>High-throughput architectural layout optimized for Tier ${product.tier} commercial processing.</span>
</div>
</div>
</div>
<!-- Deep Statistical & Velocity Telemetry (Capitalism II Style 3-Column Matrix) -->
<div class="dossier-section-title">
<iconify-icon icon="lucide:activity"></iconify-icon> Executive Economics & Production Telemetry
</div>
<div class="dossier-stats-matrix">
<!-- 1. Financial Performance Block -->
<div class="matrix-card">
<div class="matrix-header">
<iconify-icon icon="lucide:dollar-sign"></iconify-icon> Financial Performance & Share
</div>
<div class="matrix-rows">
<div class="matrix-row">
<span class="m-label">Wholesale Unit Price:</span>
<span class="m-val font-mono">${fmtCurrency(product.basePrice)}</span>
</div>
<div class="matrix-row">
<span class="m-label">Logistics Revenue Rate:</span>
<span class="m-val font-mono text-profit" id="det-rev-rate">+$${fmtNum(rates.revRate, 2)}/s</span>
</div>
<div class="matrix-row">
<span class="m-label">Projected Hourly Revenue:</span>
<span class="m-val font-mono text-profit" id="det-rev-hour">${fmtCurrency(revPerHour)}/h</span>
</div>
<div class="matrix-row">
<span class="m-label">Enterprise Revenue Share:</span>
<span class="m-val font-mono text-equity" id="det-rev-share">${fmtNum(companyRevShare, 1)}%</span>
</div>
<div class="matrix-divider"></div>
<div class="matrix-row">
<span class="m-label">Inventory Warehouse Value:</span>
<span class="m-val font-mono" id="det-stock-val">${fmtCurrency(inventoryValuation)}</span>
</div>
<div class="matrix-row">
<span class="m-label">Facility Plant Capital Value:</span>
<span class="m-val font-mono">${fmtCurrency(facilityValuation)}</span>
</div>
</div>
</div>
<!-- 2. Engineering & Production Dynamics Block -->
<div class="matrix-card">
<div class="matrix-header">
<iconify-icon icon="lucide:gauge"></iconify-icon> Industrial Dynamics & Velocity
</div>
<div class="matrix-rows">
<div class="matrix-row">
<span class="m-label">Operating Industrial Lines:</span>
<span class="m-val font-mono" id="det-lines">${pState.lines} lines (${nextM - pState.lines} to 2x)</span>
</div>
<div class="matrix-row">
<span class="m-label">Effective Cycle Time:</span>
<span class="m-val font-mono" id="det-cycle-time">${effectiveCycleTime.toFixed(2)}s <span class="text-muted">(${product.baseCycleTime.toFixed(1)}s raw)</span></span>
</div>
<div class="matrix-row">
<span class="m-label">Milestone Speed Multiplier:</span>
<span class="m-val font-mono text-profit">${milestoneMult}x Speed Boost</span>
</div>
<div class="matrix-row">
<span class="m-label">Batches Processed / Sec:</span>
<span class="m-val font-mono" id="det-cycle-rate">${fmtNum(cyclesPerSec, 2)}/s</span>
</div>
<div class="matrix-row">
<span class="m-label">Production Velocity:</span>
<span class="m-val font-mono text-profit" id="det-prod-rate">${fmtNum(unitsPerSec, 1)} units/s</span>
</div>
<div class="matrix-row">
<span class="m-label">Projected Hourly Output:</span>
<span class="m-val font-mono" id="det-prod-hour">${fmtNum(unitsPerHour, 0)} ${product.unit}/h</span>
</div>
</div>
</div>
<!-- 3. Supply Chain Flow Balance Block -->
<div class="matrix-card">
<div class="matrix-header">
<iconify-icon icon="lucide:git-pull-request"></iconify-icon> Supply Chain Flow Balance
</div>
<div class="matrix-rows">
<div class="matrix-row">
<span class="m-label">Plant Production Rate:</span>
<span class="m-val font-mono text-profit" id="det-flow-prod">+${fmtNum(unitsPerSec, 1)}/s</span>
</div>
<div class="matrix-row">
<span class="m-label">Downstream Consumption Pull:</span>
<span class="m-val font-mono text-warning" id="det-flow-consumed">-${fmtNum(netFlow.consumedRate, 1)}/s</span>
</div>
<div class="matrix-row">
<span class="m-label">Wholesale Logistics Sales:</span>
<span class="m-val font-mono text-warning" id="det-flow-sold">-${fmtNum(netFlow.soldRate, 1)}/s</span>
</div>
<div class="matrix-divider"></div>
<div class="matrix-row highlight-row">
<span class="m-label">Net Inventory Drift (Δ/s):</span>
<span class="m-val font-mono ${netFlow.netRate < -0.05 ? 'text-warning' : 'text-profit'}" id="det-flow-net">
${netFlow.netRate > 0.01 ? '+' : ''}${fmtNum(netFlow.netRate, 1)}/s
</span>
</div>
<div class="matrix-row">
<span class="m-label">Current Operating Throttle:</span>
<span class="m-val font-mono" id="det-throttle-stat">${Math.round(currentThrottle * 100)}% (${fmtNum(effectiveLines, 1)} lines active)</span>
</div>
</div>
</div>
</div>
<!-- Strategic Policies & Throttle Control -->
<div class="dossier-control-grid">
<!-- Throttle Control Widget -->
<div class="control-box">
<div class="control-header">
<iconify-icon icon="lucide:sliders"></iconify-icon> Facility Output Throttle
</div>
<p class="control-desc">Scale operating speed and raw material consumption proportionally.</p>
<div class="throttle-pills">
${throttleLevels.map(lvl => {
const isMatch = Math.abs(currentThrottle - lvl.val) < 0.01;
const offClass = lvl.val === 0.0 ? 'pill-off' : '';
return `
<button class="btn-throttle-pill ${isMatch ? `active ${offClass}` : ''}" data-product="${product.id}" data-target="${lvl.val}">
${lvl.label}
</button>
`;
}).join('')}
</div>
</div>
<!-- Policies Widget -->
<div class="control-box">
<div class="control-header">
<iconify-icon icon="lucide:truck"></iconify-icon> Logistics Fleet & Downstream Supply
</div>
<p class="control-desc">Control automated wholesale liquidation and raw material delivery permissions.</p>
<div class="policy-toggle-grid">
<button class="btn-policy-toggle ${(pState.logisticsEnabled !== false) ? 'active' : 'inactive'}" id="detail-toggle-logistics">
<iconify-icon icon="${(pState.logisticsEnabled !== false) ? 'lucide:truck' : 'lucide:truck-x'}"></iconify-icon>
<span id="detail-log-text">Wholesale Fleet: ${(pState.logisticsEnabled !== false) ? 'ENABLED' : 'PAUSED'}</span>
</button>
<button class="btn-policy-toggle ${(pState.supplyDownstreamEnabled !== false) ? 'active' : 'inactive'}" id="detail-toggle-downstream">
<iconify-icon icon="${(pState.supplyDownstreamEnabled !== false) ? 'lucide:git-pull-request' : 'lucide:git-pull-request-closed'}"></iconify-icon>
<span id="detail-down-text">Downstream Supply: ${(pState.supplyDownstreamEnabled !== false) ? 'ALLOWED' : 'BLOCKED'}</span>
</button>
</div>
<div class="policy-sub-row">
<span>Fleet Capacity: <strong>${pState.logistics || 0}</strong> fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)} u/s)</span>
<button class="btn-corp-secondary" id="detail-btn-logistics" ${canAffordLogistics ? '' : 'disabled'}>
+Fleet (${fmtCurrency(logisticsCost)})
</button>
</div>
</div>
<!-- Spot Market Instant Liquidation -->
<div class="control-box">
<div class="control-header">
<iconify-icon icon="lucide:coins"></iconify-icon> Spot Market Liquidation
</div>
<p class="control-desc">Liquidate stockpile instantly onto the open wholesale spot market.</p>
<div class="liquidation-buttons-row">
<button class="btn-corp-secondary" id="detail-btn-liq-half" ${pState.stock >= 2 ? '' : 'disabled'}>
<iconify-icon icon="lucide:pie-chart"></iconify-icon>
<span>Sell 50% (<strong id="det-liq-half-val">${fmtCurrency(halfLiquidationValue)}</strong>)</span>
</button>
<button class="btn-primary-corp" id="detail-btn-liq-all" ${pState.stock >= 1 ? '' : 'disabled'}>
<iconify-icon icon="lucide:check-circle-2"></iconify-icon>
<span>Sell 100% (<strong id="det-liq-all-val">${fmtCurrency(totalLiquidationValue)}</strong>)</span>
</button>
</div>
</div>
</div>
<!-- Upstream & Downstream Supply Chain Ecosystem -->
<div class="dossier-section-title">
<iconify-icon icon="lucide:network"></iconify-icon> Supply Chain Ecosystem
</div>
<div class="dossier-chain-grid">
<!-- Upstream Direct Inputs -->
<div class="chain-column-box">
<div class="chain-col-header">
<iconify-icon icon="lucide:arrow-down-left"></iconify-icon> Direct Input Dependencies (Upstream)
</div>
${(!product.inputs || product.inputs.length === 0) ? `
<div class="chain-empty-notice">
<iconify-icon icon="lucide:sparkles"></iconify-icon>
<span>Primary Commodity: Direct extraction from natural deposits with no upstream dependencies.</span>
</div>
` : `
<div class="chain-cards-list">
${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 `
<div class="chain-flow-card ${isLow ? 'low-stock' : ''}" data-product="${parent.id}">
<div class="flow-card-left">
<div class="flow-icon" style="background: ${parent.color}15; color: ${parent.color};">
<iconify-icon icon="${parent.icon}"></iconify-icon>
</div>
<div>
<h4 class="flow-name">${parent.name}</h4>
<div class="flow-req-sub">
${inp.amount} per batch &bull; Running at <strong class="font-mono">${fmtNum(consumptionPerSec, 1)}/s</strong>
</div>
</div>
</div>
<div class="flow-card-right">
<span class="flow-stock font-mono ${isLow ? 'text-warning' : ''}">
Stock: ${fmtNum(parentState.stock, 0)} ${parent.unit}
</span>
<button class="btn-jump-unit" data-product="${parent.id}">View Dossier &rarr;</button>
</div>
</div>
`;
}).join('')}
</div>
`}
</div>
<!-- Downstream Consumers -->
<div class="chain-column-box">
<div class="chain-col-header">
<iconify-icon icon="lucide:arrow-up-right"></iconify-icon> Downstream Consumers (Industrial Demand)
</div>
${downstream.length === 0 ? `
<div class="chain-empty-notice">
<iconify-icon icon="lucide:package-check"></iconify-icon>
<span>End-tier Finished Product: Distributed directly to global commercial markets.</span>
</div>
` : `
<div class="chain-cards-list">
${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 `
<div class="chain-flow-card" data-product="${child.id}">
<div class="flow-card-left">
<div class="flow-icon" style="background: ${child.color}15; color: ${child.color};">
<iconify-icon icon="${child.icon}"></iconify-icon>
</div>
<div>
<h4 class="flow-name">${child.name}</h4>
<div class="flow-req-sub">
Requires ${inpMatch?.amount || 1} ${product.unit} per batch &bull; Tier ${child.tier}
</div>
</div>
</div>
<div class="flow-card-right">
<span class="chain-badge ${isUnlocked ? 'badge-open' : 'badge-locked'}">
${isUnlocked ? `${childState.lines} lines active` : 'Locked Blueprint'}
</span>
<button class="btn-jump-unit" data-product="${child.id}">View Dossier &rarr;</button>
</div>
</div>
`;
}).join('')}
</div>
`}
</div>
</div>
</div>
`;
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}` : ''}`;
});
}