601 lines
32 KiB
JavaScript
601 lines
32 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 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 contribution
|
|
const facilityValuation = (pState.lines || 0) * product.baseFacilityCost;
|
|
const inventoryValuation = (pState.stock || 0) * product.basePrice;
|
|
|
|
// Dual Photographic URLs (with standardized 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 -->
|
|
<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}</span>
|
|
<span class="dossier-id-tag font-mono">SYS-ID: ${product.id.toUpperCase()}</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} Blueprint • ${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>
|
|
|
|
<!-- Dual Photographic Assets Section -->
|
|
<div class="dossier-photo-grid">
|
|
<!-- 1. Facility Architecture Photo Card -->
|
|
<div class="dossier-photo-card">
|
|
<div class="photo-card-header">
|
|
<span class="photo-tag"><iconify-icon icon="lucide:building-2"></iconify-icon> Industrial Facility Rendering</span>
|
|
<span class="photo-spec font-mono">${product.facilityName}</span>
|
|
</div>
|
|
<div class="photo-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">Blueprint asset pending in /assets/images/facilities/${product.id}_facility.jpg</span>
|
|
</div>
|
|
</div>
|
|
<div class="photo-caption">
|
|
<span>High-throughput architectural layout optimized for Tier ${product.tier} processing.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. Commodity Specimen Photo Card -->
|
|
<div class="dossier-photo-card">
|
|
<div class="photo-card-header">
|
|
<span class="photo-tag"><iconify-icon icon="lucide:box"></iconify-icon> Material / 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">Specimen asset pending in /assets/images/products/${product.id}_product.jpg</span>
|
|
</div>
|
|
</div>
|
|
<div class="photo-caption">
|
|
<span>Commercial wholesale specimen traded at ${fmtCurrency(product.basePrice)} per ${product.unit}.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Deep Statistical & Velocity Telemetry (Data Nerd Grid) -->
|
|
<div class="dossier-section-title">
|
|
<iconify-icon icon="lucide:activity"></iconify-icon> Operational & Financial Telemetry
|
|
</div>
|
|
|
|
<div class="dossier-stats-matrix">
|
|
<!-- Financial Block -->
|
|
<div class="matrix-card">
|
|
<div class="matrix-header">
|
|
<iconify-icon icon="lucide:dollar-sign"></iconify-icon> Financial Performance
|
|
</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">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 Asset Valuation:</span>
|
|
<span class="m-val font-mono">${fmtCurrency(facilityValuation)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Engineering & Velocity Block -->
|
|
<div class="matrix-card">
|
|
<div class="matrix-header">
|
|
<iconify-icon icon="lucide:gauge"></iconify-icon> Production Dynamics
|
|
</div>
|
|
<div class="matrix-rows">
|
|
<div class="matrix-row">
|
|
<span class="m-label">Active 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">Base Cycle Duration:</span>
|
|
<span class="m-val font-mono">${product.baseCycleTime.toFixed(1)}s (raw)</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</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">Hourly Volume Output:</span>
|
|
<span class="m-val font-mono" id="det-prod-hour">${fmtNum(unitsPerHour, 0)} ${product.unit}/h</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Net Supply Chain Flow Block -->
|
|
<div class="matrix-card">
|
|
<div class="matrix-header">
|
|
<iconify-icon icon="lucide:git-pull-request"></iconify-icon> Supply Chain Balance Formula
|
|
</div>
|
|
<div class="matrix-rows">
|
|
<div class="matrix-row">
|
|
<span class="m-label">Factory 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:</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">Logistics Wholesale 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 Flow (Δ):</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>
|
|
</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 • 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 →</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 • 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 →</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'}`;
|
|
}
|
|
|
|
// 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}` : ''}`;
|
|
});
|
|
}
|