235 lines
11 KiB
JavaScript
235 lines
11 KiB
JavaScript
// Production UI: Compact Facility Card (HTML Mount and Fine-grained DOM update)
|
|
// High-density overview cards with photographic thumbnail, live rates, bottleneck status, and quick-actions
|
|
|
|
import { getProduct, getNextMilestone, getUnitMilestoneMultiplier, SECTORS } from '../../data/products.js';
|
|
import { fmtCurrency, fmtNum } from '../../core/formatters.js';
|
|
import { calculateMultiCost, getProductOutputRates } from './ratesCalculator.js';
|
|
|
|
export function renderCompactFacilityCardHtml(p, pState, state, buyMode) {
|
|
const sector = SECTORS.find(s => s.id === p.sector);
|
|
const isSelected = state.selectedProductId === p.id;
|
|
const productImgPath = `assets/images/products/${p.id}_product.jpg`;
|
|
|
|
if (!pState.unlocked) {
|
|
const canAffordUnlock = state.cash >= p.unlockCost;
|
|
return `
|
|
<div class="compact-card locked-compact-card ${isSelected ? 'selected' : ''}" id="compact-card-${p.id}" data-product="${p.id}">
|
|
<div class="compact-thumb-wrap">
|
|
<img src="${productImgPath}" alt="${p.name}" class="compact-thumb-img" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
|
<div class="compact-thumb-fallback" style="background: ${p.color}15; color: ${p.color}; display: none;">
|
|
<iconify-icon icon="${p.icon}"></iconify-icon>
|
|
</div>
|
|
<div class="compact-thumb-lock-overlay">
|
|
<iconify-icon icon="lucide:lock"></iconify-icon>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="compact-info-col">
|
|
<div class="compact-meta-row">
|
|
<span class="compact-sector-badge" style="color: ${sector?.color || '#64748b'};">
|
|
${sector?.name || 'Industry'} • T${p.tier}
|
|
</span>
|
|
<span class="compact-status-badge badge-idle">Permit Needed</span>
|
|
</div>
|
|
<h4 class="compact-name">${p.name}</h4>
|
|
<span class="compact-sub">${p.facilityName} • Wholesale: ${fmtCurrency(p.basePrice)}</span>
|
|
</div>
|
|
|
|
<div class="compact-actions-col">
|
|
<button class="btn-primary-corp btn-compact-unlock" id="btn-compact-unlock-${p.id}" data-product="${p.id}" ${canAffordUnlock ? '' : 'disabled'} title="Acquire Industrial Permit">
|
|
<iconify-icon icon="lucide:lock-open"></iconify-icon>
|
|
<span>${fmtCurrency(p.unlockCost)}</span>
|
|
</button>
|
|
<button class="btn-card-detail btn-compact-detail" data-product="${p.id}" title="Open Full Technical Dossier">
|
|
<iconify-icon icon="lucide:info"></iconify-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const nextM = getNextMilestone(pState.lines || 0);
|
|
const rates = getProductOutputRates(p, pState, state);
|
|
const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines || 0);
|
|
const clientRep = state.repAllocations?.clients || 0;
|
|
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
|
|
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount);
|
|
const canAffordBuy = state.cash >= calc.cost;
|
|
const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
|
|
|
|
let statusClass = 'badge-success';
|
|
let statusLabel = `${milestoneMult}x Speed`;
|
|
let statusIcon = 'lucide:check-circle';
|
|
|
|
if (currentThrottle <= 0.0) {
|
|
statusClass = 'badge-idle';
|
|
statusLabel = 'Paused (0%)';
|
|
statusIcon = 'lucide:pause-circle';
|
|
} else if (pState.isBottlenecked) {
|
|
statusClass = 'badge-warning';
|
|
statusLabel = 'Starved';
|
|
statusIcon = 'lucide:alert-triangle';
|
|
} else if (!pState.lines || pState.lines === 0) {
|
|
statusClass = 'badge-idle';
|
|
statusLabel = '0 Lines';
|
|
statusIcon = 'lucide:circle-slash';
|
|
}
|
|
|
|
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
|
|
|
return `
|
|
<div class="compact-card active-compact-card ${isSelected ? 'selected' : ''}" id="compact-card-${p.id}" data-product="${p.id}">
|
|
<!-- Miniature Cycle Bar Indicator -->
|
|
<div class="compact-progress-bar" id="compact-progress-${p.id}" style="width: ${progressPct}%; background-color: ${p.color};"></div>
|
|
|
|
<div class="compact-thumb-wrap">
|
|
<img src="${productImgPath}" alt="${p.name}" class="compact-thumb-img" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
|
<div class="compact-thumb-fallback" style="background: ${p.color}15; color: ${p.color}; display: none;">
|
|
<iconify-icon icon="${p.icon}"></iconify-icon>
|
|
</div>
|
|
<span class="compact-tier-pill">T${p.tier}</span>
|
|
</div>
|
|
|
|
<div class="compact-info-col">
|
|
<div class="compact-meta-row">
|
|
<span class="compact-sector-badge" style="color: ${sector?.color || '#64748b'};">
|
|
${sector?.name}
|
|
</span>
|
|
<span class="compact-status-badge ${statusClass}" id="compact-badge-${p.id}">
|
|
<iconify-icon icon="${statusIcon}"></iconify-icon>
|
|
<span id="compact-badge-text-${p.id}">${statusLabel}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="compact-title-row">
|
|
<h4 class="compact-name">${p.name}</h4>
|
|
<span class="compact-lines-badge" id="compact-lines-${p.id}"><strong>${pState.lines}</strong> lines</span>
|
|
</div>
|
|
|
|
<div class="compact-stats-row font-mono">
|
|
<span class="compact-stat-item" title="Stockpile">
|
|
<iconify-icon icon="lucide:box"></iconify-icon>
|
|
<strong id="compact-stock-${p.id}">${fmtNum(pState.stock || 0, 0)}</strong>
|
|
</span>
|
|
<span class="compact-stat-divider">•</span>
|
|
<span class="compact-stat-item ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}" id="compact-rate-units-${p.id}" title="Production Velocity">
|
|
${rates.isBottlenecked ? '0.0/s' : `${fmtNum(rates.prodRate || 0, 1)}/s`}
|
|
</span>
|
|
<span class="compact-stat-divider">•</span>
|
|
<span class="compact-stat-item text-profit" id="compact-rate-rev-${p.id}" title="Logistics Wholesale Revenue">
|
|
+$${fmtNum(rates.revRate || 0, 2)}/s
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="compact-actions-col">
|
|
<button class="btn-primary-corp btn-compact-expand" id="btn-compact-expand-${p.id}" data-product="${p.id}" ${canAffordBuy ? '' : 'disabled'} title="Expand production lines">
|
|
<iconify-icon icon="lucide:plus"></iconify-icon>
|
|
<span id="compact-expand-text-${p.id}">+${calc.count} (${fmtCurrency(calc.cost)})</span>
|
|
</button>
|
|
<button class="btn-card-detail btn-compact-detail" data-product="${p.id}" title="Open Full Technical Dossier">
|
|
<iconify-icon icon="lucide:info"></iconify-icon>
|
|
<span>Details</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export function updateCompactFacilityCardDom(p, pState, state, engine) {
|
|
if (!pState.unlocked) {
|
|
const canAfford = state.cash >= p.unlockCost;
|
|
const btn = document.getElementById(`btn-compact-unlock-${p.id}`);
|
|
if (btn && btn.disabled === canAfford) {
|
|
btn.disabled = !canAfford;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const buyMode = state.buyMode || '1';
|
|
const rates = getProductOutputRates(p, pState, state);
|
|
const clientRep = state.repAllocations?.clients || 0;
|
|
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
|
|
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount);
|
|
const canAffordBuy = state.cash >= calc.cost;
|
|
const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
|
|
|
|
// Progress bar
|
|
const elProg = document.getElementById(`compact-progress-${p.id}`);
|
|
if (elProg) {
|
|
const pct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
|
elProg.style.width = `${pct}%`;
|
|
}
|
|
|
|
// Lines
|
|
const elLines = document.getElementById(`compact-lines-${p.id}`);
|
|
if (elLines) {
|
|
const txt = `<strong>${pState.lines || 0}</strong> lines`;
|
|
if (elLines.innerHTML !== txt) elLines.innerHTML = txt;
|
|
}
|
|
|
|
// Stock
|
|
const elStock = document.getElementById(`compact-stock-${p.id}`);
|
|
if (elStock) {
|
|
const val = fmtNum(pState.stock || 0, 0);
|
|
if (elStock.textContent !== val) elStock.textContent = val;
|
|
}
|
|
|
|
// Production Rate
|
|
const elUnits = document.getElementById(`compact-rate-units-${p.id}`);
|
|
if (elUnits) {
|
|
const txt = rates.isBottlenecked ? '0.0/s' : `${fmtNum(rates.prodRate || 0, 1)}/s`;
|
|
if (elUnits.textContent !== txt) elUnits.textContent = txt;
|
|
elUnits.className = `compact-stat-item ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}`;
|
|
}
|
|
|
|
// Logistics Revenue Rate
|
|
const elRev = document.getElementById(`compact-rate-rev-${p.id}`);
|
|
if (elRev) {
|
|
const txt = `+$${fmtNum(rates.revRate || 0, 2)}/s`;
|
|
if (elRev.textContent !== txt) elRev.textContent = txt;
|
|
}
|
|
|
|
// Status Badge
|
|
const elBadge = document.getElementById(`compact-badge-${p.id}`);
|
|
const elBadgeText = document.getElementById(`compact-badge-text-${p.id}`);
|
|
if (elBadge && elBadgeText) {
|
|
let statusClass = 'badge-success';
|
|
let statusLabel = `${rates.milestoneMult}x Speed`;
|
|
let statusIcon = 'lucide:check-circle';
|
|
|
|
if (currentThrottle <= 0.0) {
|
|
statusClass = 'badge-idle';
|
|
statusLabel = 'Paused';
|
|
statusIcon = 'lucide:pause-circle';
|
|
} else if (rates.isBottlenecked) {
|
|
statusClass = 'badge-warning';
|
|
statusLabel = 'Starved';
|
|
statusIcon = 'lucide:alert-triangle';
|
|
} else if (!pState.lines || pState.lines === 0) {
|
|
statusClass = 'badge-idle';
|
|
statusLabel = '0 Lines';
|
|
statusIcon = 'lucide:circle-slash';
|
|
}
|
|
|
|
if (elBadgeText.textContent !== statusLabel) elBadgeText.textContent = statusLabel;
|
|
elBadge.className = `compact-status-badge ${statusClass}`;
|
|
const iconEl = elBadge.querySelector('iconify-icon');
|
|
if (iconEl && iconEl.getAttribute('icon') !== statusIcon) {
|
|
iconEl.setAttribute('icon', statusIcon);
|
|
}
|
|
}
|
|
|
|
// Buy Button
|
|
const btnExpand = document.getElementById(`btn-compact-expand-${p.id}`);
|
|
const spanExpand = document.getElementById(`compact-expand-text-${p.id}`);
|
|
if (btnExpand) {
|
|
if (btnExpand.disabled === canAffordBuy) {
|
|
btnExpand.disabled = !canAffordBuy;
|
|
}
|
|
if (spanExpand) {
|
|
const expText = `+${calc.count} (${fmtCurrency(calc.cost)})`;
|
|
if (spanExpand.textContent !== expText) spanExpand.textContent = expText;
|
|
}
|
|
}
|
|
}
|