ProductionGame/frontend/js/ui/production/compactFacilityCard.js

231 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'} &bull; 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} &bull; 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 calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode);
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">&bull;</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">&bull;</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 calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode);
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;
}
}
}