243 lines
12 KiB
JavaScript
243 lines
12 KiB
JavaScript
// Production - The Idle Game: Production Floor View
|
|
|
|
import { PRODUCTS, SECTORS, getProduct, getNextMilestone, getUnitMilestoneMultiplier } from '../data/products.js';
|
|
import { fmtCurrency, fmtNum } from '../core/formatters.js';
|
|
|
|
export function calculateMultiCost(baseCost, currentCount, multiplier) {
|
|
// Standard incremental scaling: cost = baseCost * 1.15^count
|
|
// For n purchases: sum(baseCost * 1.15^(count + i))
|
|
const growth = 1.15;
|
|
let countToBuy = 1;
|
|
|
|
if (multiplier === '1') countToBuy = 1;
|
|
else if (multiplier === '10') countToBuy = 10;
|
|
else if (multiplier === '100') countToBuy = 100;
|
|
else if (multiplier === 'next') {
|
|
const nextTarget = getNextMilestone(currentCount);
|
|
countToBuy = Math.max(1, nextTarget - currentCount);
|
|
} else if (multiplier === 'max') {
|
|
countToBuy = 1; // Will be handled dynamically in loop
|
|
}
|
|
|
|
let totalCost = 0;
|
|
for (let i = 0; i < countToBuy; i++) {
|
|
totalCost += baseCost * Math.pow(growth, currentCount + i);
|
|
}
|
|
return { count: countToBuy, cost: totalCost };
|
|
}
|
|
|
|
export function renderProductionFloor(container, state, onAction) {
|
|
const selectedSector = state.selectedSector || 'all';
|
|
const buyMode = state.buyMode || '1';
|
|
|
|
const filteredProducts = PRODUCTS.filter(p => {
|
|
if (selectedSector === 'all') return true;
|
|
return p.sector === selectedSector;
|
|
});
|
|
|
|
let html = `
|
|
<div class="production-toolbar">
|
|
<div class="sector-tabs-scroll">
|
|
<button class="sector-pill ${selectedSector === 'all' ? 'active' : ''}" data-sector="all">
|
|
<iconify-icon icon="lucide:layout-grid"></iconify-icon>
|
|
<span>All Sectors</span>
|
|
</button>
|
|
${SECTORS.map(s => `
|
|
<button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}">
|
|
<iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon>
|
|
<span>${s.name}</span>
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="production-grid">
|
|
`;
|
|
|
|
for (const p of filteredProducts) {
|
|
const pState = state.products[p.id] || { unlocked: false, lines: 0, stock: 0, cycleProgress: 0, isBottlenecked: false };
|
|
const sector = SECTORS.find(s => s.id === p.sector);
|
|
const isSelected = state.selectedProductId === p.id;
|
|
|
|
if (!pState.unlocked) {
|
|
// Locked Card
|
|
const canAffordUnlock = state.cash >= p.unlockCost;
|
|
html += `
|
|
<div class="facility-card locked-card ${isSelected ? 'selected' : ''}" data-product="${p.id}">
|
|
<div class="facility-header">
|
|
<div class="facility-icon-wrap" style="background: ${p.color}15; color: ${p.color};">
|
|
<iconify-icon icon="${p.icon}" class="icon-lg"></iconify-icon>
|
|
</div>
|
|
<div class="facility-title-wrap">
|
|
<span class="sector-badge" style="color: ${sector?.color || '#64748b'};">
|
|
${sector?.name || 'Industry'} • Tier ${p.tier}
|
|
</span>
|
|
<h3 class="facility-name">${p.name}</h3>
|
|
</div>
|
|
</div>
|
|
<p class="facility-desc">${p.description}</p>
|
|
<div class="locked-footer">
|
|
<button class="btn-primary-corp btn-unlock" data-product="${p.id}" ${canAffordUnlock ? '' : 'disabled'}>
|
|
<iconify-icon icon="lucide:lock-open"></iconify-icon>
|
|
<span>Acquire Permit (${fmtCurrency(p.unlockCost)})</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
continue;
|
|
}
|
|
|
|
// Unlocked Active Card
|
|
const nextM = getNextMilestone(pState.lines);
|
|
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
|
|
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
|
|
const canAffordBuy = state.cash >= calc.cost;
|
|
const managerCost = p.baseManagerCost;
|
|
const canAffordManager = state.cash >= managerCost;
|
|
const logisticsCost = p.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0);
|
|
const canAffordLogistics = state.cash >= logisticsCost;
|
|
|
|
// Input Requirements status
|
|
const isRaw = !p.inputs || p.inputs.length === 0;
|
|
let inputsHtml = '';
|
|
if (!isRaw) {
|
|
inputsHtml = `
|
|
<div class="inputs-strip">
|
|
<span class="inputs-label">Inputs per line:</span>
|
|
<div class="inputs-chips">
|
|
${p.inputs.map(inp => {
|
|
const inpProduct = getProduct(inp.id);
|
|
const currentStock = state.products[inp.id]?.stock || 0;
|
|
const neededPerLine = inp.amount * pState.lines;
|
|
const isLow = currentStock < neededPerLine;
|
|
return `
|
|
<span class="input-chip ${isLow ? 'low-stock' : ''}" title="${inpProduct?.name}">
|
|
<iconify-icon icon="${inpProduct?.icon || 'lucide:box'}"></iconify-icon>
|
|
${inpProduct?.name}: <strong>${fmtNum(currentStock, 0)}</strong> / ${fmtNum(neededPerLine, 0)}
|
|
</span>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
let statusBadge = '';
|
|
if (pState.isBottlenecked) {
|
|
statusBadge = `<span class="status-badge badge-warning"><iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs</span>`;
|
|
} else if (pState.lines > 0) {
|
|
statusBadge = `<span class="status-badge badge-success"><iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)</span>`;
|
|
} else {
|
|
statusBadge = `<span class="status-badge badge-idle">Idle (0 Lines)</span>`;
|
|
}
|
|
|
|
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
|
|
|
html += `
|
|
<div class="facility-card active-card ${isSelected ? 'selected' : ''}" data-product="${p.id}">
|
|
<div class="facility-header">
|
|
<div class="facility-icon-wrap" style="background: ${p.color}15; color: ${p.color};">
|
|
<iconify-icon icon="${p.icon}" class="icon-lg"></iconify-icon>
|
|
</div>
|
|
<div class="facility-title-wrap">
|
|
<div class="facility-meta-row">
|
|
<span class="sector-badge" style="color: ${sector?.color || '#64748b'};">
|
|
${sector?.name} • Tier ${p.tier}
|
|
</span>
|
|
${statusBadge}
|
|
</div>
|
|
<h3 class="facility-name">${p.name}</h3>
|
|
<span class="facility-type-sub">${p.facilityName} • <strong>${pState.lines}</strong> lines (${nextM - pState.lines} to 2x)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cycle Progress Bar -->
|
|
<div class="cycle-bar-track">
|
|
<div class="cycle-bar-fill" style="width: ${progressPct}%; background-color: ${p.color};"></div>
|
|
</div>
|
|
|
|
<!-- Stock and Inventory Summary -->
|
|
<div class="facility-stats-grid">
|
|
<div class="stat-cell">
|
|
<span class="stat-label">Stockpile:</span>
|
|
<span class="stat-value font-mono"><strong>${fmtNum(pState.stock, 0)}</strong> ${p.unit}</span>
|
|
</div>
|
|
<div class="stat-cell">
|
|
<span class="stat-label">Wholesale Price:</span>
|
|
<span class="stat-value font-mono">${fmtCurrency(p.basePrice)}</span>
|
|
</div>
|
|
<div class="stat-cell">
|
|
<span class="stat-label">Logistics Fleet:</span>
|
|
<span class="stat-value font-mono">${pState.logistics || 0} fleets</span>
|
|
</div>
|
|
</div>
|
|
|
|
${inputsHtml}
|
|
|
|
<!-- Facility Actions Bar -->
|
|
<div class="facility-actions-row">
|
|
<button class="btn-corp-action btn-cycle" data-product="${p.id}" title="Trigger manual batch">
|
|
<iconify-icon icon="lucide:play"></iconify-icon>
|
|
<span>Run Batch</span>
|
|
</button>
|
|
|
|
<button class="btn-primary-corp btn-expand" data-product="${p.id}" ${canAffordBuy ? '' : 'disabled'}>
|
|
<iconify-icon icon="lucide:plus-circle"></iconify-icon>
|
|
<span>+${calc.count} Lines (${fmtCurrency(calc.cost)})</span>
|
|
</button>
|
|
|
|
${!pState.manager ? `
|
|
<button class="btn-corp-secondary btn-hire-mgr" data-product="${p.id}" ${canAffordManager ? '' : 'disabled'} title="Automates production continuously">
|
|
<iconify-icon icon="lucide:user-check"></iconify-icon>
|
|
<span>Hire Lead (${fmtCurrency(managerCost)})</span>
|
|
</button>
|
|
` : `
|
|
<span class="mgr-hired-badge" title="Automated Lead Active">
|
|
<iconify-icon icon="lucide:bot"></iconify-icon> Auto
|
|
</span>
|
|
`}
|
|
|
|
<button class="btn-corp-secondary btn-expand-logistics" data-product="${p.id}" ${canAffordLogistics ? '' : 'disabled'} title="Increase passive sales capacity">
|
|
<iconify-icon icon="lucide:truck"></iconify-icon>
|
|
<span>+Logistics (${fmtCurrency(logisticsCost)})</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
container.innerHTML = html;
|
|
|
|
// Attach delegated events
|
|
container.querySelectorAll('.sector-pill').forEach(btn => {
|
|
btn.onclick = () => onAction({ type: 'SELECT_SECTOR', sector: btn.dataset.sector });
|
|
});
|
|
|
|
container.querySelectorAll('.facility-card').forEach(card => {
|
|
card.onclick = (e) => {
|
|
if (e.target.closest('button')) return; // Ignore clicks on action buttons
|
|
onAction({ type: 'SELECT_PRODUCT', productId: card.dataset.product });
|
|
};
|
|
});
|
|
|
|
container.querySelectorAll('.btn-unlock').forEach(btn => {
|
|
btn.onclick = () => onAction({ type: 'UNLOCK_PRODUCT', productId: btn.dataset.product });
|
|
});
|
|
|
|
container.querySelectorAll('.btn-cycle').forEach(btn => {
|
|
btn.onclick = () => onAction({ type: 'MANUAL_CYCLE', productId: btn.dataset.product });
|
|
});
|
|
|
|
container.querySelectorAll('.btn-expand').forEach(btn => {
|
|
btn.onclick = () => onAction({ type: 'BUY_LINES', productId: btn.dataset.product });
|
|
});
|
|
|
|
container.querySelectorAll('.btn-hire-mgr').forEach(btn => {
|
|
btn.onclick = () => onAction({ type: 'HIRE_MANAGER', productId: btn.dataset.product });
|
|
});
|
|
|
|
container.querySelectorAll('.btn-expand-logistics').forEach(btn => {
|
|
btn.onclick = () => onAction({ type: 'BUY_LOGISTICS', productId: btn.dataset.product });
|
|
});
|
|
}
|