feat(sectors): add total output metrics per industry across sidebar, tabs, summary banner, and all-sectors grid
This commit is contained in:
parent
d038cd1879
commit
67e13a40b1
5 changed files with 478 additions and 55 deletions
|
|
@ -346,19 +346,212 @@ body {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sector-item {
|
.sidebar-sector-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 7px 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.15s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sector-btn:hover {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sector-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sector-left iconify-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sector-rate {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--emerald-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-pill-rate {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--emerald-dark);
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sector Output Summary Banner */
|
||||||
|
.sector-summary-banner {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-summary-main {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-summary-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-summary-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-summary-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-summary-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-metrics-strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
background: var(--bg-surface-sub);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-metric-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec-metric-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec-metric-val {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-profit {
|
||||||
|
color: var(--emerald-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All Sectors Overview Grid */
|
||||||
|
.all-sectors-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-mini-card {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-mini-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-title-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-sub {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
background: var(--bg-surface-sub);
|
||||||
|
padding: 6px 8px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sector-item iconify-icon {
|
.mini-stat-cell {
|
||||||
font-size: 1rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-stat-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-stat-cell strong {
|
||||||
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Center Main Viewport */
|
/* Center Main Viewport */
|
||||||
|
|
|
||||||
|
|
@ -112,30 +112,48 @@
|
||||||
|
|
||||||
<div class="sidebar-section-title">Industry Sectors</div>
|
<div class="sidebar-section-title">Industry Sectors</div>
|
||||||
<div class="sidebar-sectors-list">
|
<div class="sidebar-sectors-list">
|
||||||
<div class="sidebar-sector-item">
|
<button class="sidebar-sector-btn" data-sector="agriculture">
|
||||||
|
<div class="sidebar-sector-left">
|
||||||
<iconify-icon icon="lucide:wheat" style="color: #16a34a;"></iconify-icon>
|
<iconify-icon icon="lucide:wheat" style="color: #16a34a;"></iconify-icon>
|
||||||
<span>Agriculture & Food</span>
|
<span>Agriculture</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-sector-item">
|
<span class="sidebar-sector-rate font-mono" id="sb-rate-agriculture">+$0.00/s</span>
|
||||||
|
</button>
|
||||||
|
<button class="sidebar-sector-btn" data-sector="materials">
|
||||||
|
<div class="sidebar-sector-left">
|
||||||
<iconify-icon icon="lucide:trees" style="color: #ca8a04;"></iconify-icon>
|
<iconify-icon icon="lucide:trees" style="color: #ca8a04;"></iconify-icon>
|
||||||
<span>Forestry & Materials</span>
|
<span>Forestry</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-sector-item">
|
<span class="sidebar-sector-rate font-mono" id="sb-rate-materials">+$0.00/s</span>
|
||||||
|
</button>
|
||||||
|
<button class="sidebar-sector-btn" data-sector="mining">
|
||||||
|
<div class="sidebar-sector-left">
|
||||||
<iconify-icon icon="lucide:pickaxe" style="color: #475569;"></iconify-icon>
|
<iconify-icon icon="lucide:pickaxe" style="color: #475569;"></iconify-icon>
|
||||||
<span>Mining & Metallurgy</span>
|
<span>Mining</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-sector-item">
|
<span class="sidebar-sector-rate font-mono" id="sb-rate-mining">+$0.00/s</span>
|
||||||
|
</button>
|
||||||
|
<button class="sidebar-sector-btn" data-sector="chemicals">
|
||||||
|
<div class="sidebar-sector-left">
|
||||||
<iconify-icon icon="lucide:flask-conical" style="color: #0284c7;"></iconify-icon>
|
<iconify-icon icon="lucide:flask-conical" style="color: #0284c7;"></iconify-icon>
|
||||||
<span>Chemicals & Polymers</span>
|
<span>Chemicals</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-sector-item">
|
<span class="sidebar-sector-rate font-mono" id="sb-rate-chemicals">+$0.00/s</span>
|
||||||
|
</button>
|
||||||
|
<button class="sidebar-sector-btn" data-sector="electronics">
|
||||||
|
<div class="sidebar-sector-left">
|
||||||
<iconify-icon icon="lucide:cpu" style="color: #7c3aed;"></iconify-icon>
|
<iconify-icon icon="lucide:cpu" style="color: #7c3aed;"></iconify-icon>
|
||||||
<span>High-Tech Electronics</span>
|
<span>Electronics</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-sector-item">
|
<span class="sidebar-sector-rate font-mono" id="sb-rate-electronics">+$0.00/s</span>
|
||||||
|
</button>
|
||||||
|
<button class="sidebar-sector-btn" data-sector="heavy_industry">
|
||||||
|
<div class="sidebar-sector-left">
|
||||||
<iconify-icon icon="lucide:truck" style="color: #ea580c;"></iconify-icon>
|
<iconify-icon icon="lucide:truck" style="color: #ea580c;"></iconify-icon>
|
||||||
<span>Heavy Industry</span>
|
<span>Heavy Industry</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="sidebar-sector-rate font-mono" id="sb-rate-heavy_industry">+$0.00/s</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
import { GState } from './core/state.js';
|
import { GState } from './core/state.js';
|
||||||
import { SimulationEngine } from './core/engine.js';
|
import { SimulationEngine } from './core/engine.js';
|
||||||
import { SyncManager } from './core/sync.js';
|
import { SyncManager } from './core/sync.js';
|
||||||
import { fmtCurrency, fmtNum, fmtRate, getNotation, setNotation } from './core/formatters.js';
|
import { PRODUCTS, SECTORS, getProduct } from './data/products.js';
|
||||||
import { PRODUCTS, getProduct } from './data/products.js';
|
|
||||||
import { mountProductionFloor, updateProductionFloor, calculateMultiCost } from './ui/productionView.js';
|
import { mountProductionFloor, updateProductionFloor, calculateMultiCost } from './ui/productionView.js';
|
||||||
import { mountContractsBoard, updateContractsBoard } from './ui/contractsView.js';
|
import { mountContractsBoard, updateContractsBoard } from './ui/contractsView.js';
|
||||||
import { mountBoardroom, updateBoardroom } from './ui/boardroomView.js';
|
import { mountBoardroom, updateBoardroom } from './ui/boardroomView.js';
|
||||||
|
|
@ -100,6 +99,19 @@ class ProductionApp {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sidebar Sector Buttons
|
||||||
|
document.querySelectorAll('.sidebar-sector-btn').forEach(btn => {
|
||||||
|
btn.onclick = () => {
|
||||||
|
const sector = btn.dataset.sector;
|
||||||
|
this.sm.state.activeTab = 'production';
|
||||||
|
this.sm.state.selectedSector = sector;
|
||||||
|
document.querySelectorAll('.nav-btn, .nav-tab').forEach(b => {
|
||||||
|
b.classList.toggle('active', b.dataset.tab === 'production');
|
||||||
|
});
|
||||||
|
this.mountActiveView();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// User Profile & Settings
|
// User Profile & Settings
|
||||||
const userBadge = document.getElementById('user-badge');
|
const userBadge = document.getElementById('user-badge');
|
||||||
if (userBadge) {
|
if (userBadge) {
|
||||||
|
|
@ -338,6 +350,20 @@ class ProductionApp {
|
||||||
if (userBadge) {
|
if (userBadge) {
|
||||||
userBadge.textContent = this.sync.isLoggedIn() ? `[${this.sync.getUsername()}]` : `[GUEST]`;
|
userBadge.textContent = this.sync.isLoggedIn() ? `[${this.sync.getUsername()}]` : `[GUEST]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update sidebar sector rate badges
|
||||||
|
if (this.engine.sectorStats) {
|
||||||
|
for (const s of SECTORS) {
|
||||||
|
const stats = this.engine.sectorStats[s.id];
|
||||||
|
if (stats) {
|
||||||
|
const elRate = document.getElementById(`sb-rate-${s.id}`);
|
||||||
|
if (elRate) {
|
||||||
|
const val = `+$${fmtNum(stats.revenueRate, 2)}/s`;
|
||||||
|
if (elRate.textContent !== val) elRate.textContent = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -353,7 +379,7 @@ class ProductionApp {
|
||||||
this.currentMountedSector = sector;
|
this.currentMountedSector = sector;
|
||||||
|
|
||||||
if (tab === 'production') {
|
if (tab === 'production') {
|
||||||
mountProductionFloor(container, this.sm.state, (a) => this.handleAction(a));
|
mountProductionFloor(container, this.sm.state, (a) => this.handleAction(a), this.engine);
|
||||||
} else if (tab === 'contracts') {
|
} else if (tab === 'contracts') {
|
||||||
mountContractsBoard(container, this.sm.state, (a) => this.handleAction(a));
|
mountContractsBoard(container, this.sm.state, (a) => this.handleAction(a));
|
||||||
} else if (tab === 'boardroom') {
|
} else if (tab === 'boardroom') {
|
||||||
|
|
@ -388,7 +414,7 @@ class ProductionApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab === 'production') {
|
if (tab === 'production') {
|
||||||
updateProductionFloor(container, this.sm.state);
|
updateProductionFloor(container, this.sm.state, this.engine);
|
||||||
} else if (tab === 'contracts') {
|
} else if (tab === 'contracts') {
|
||||||
updateContractsBoard(container, this.sm.state, (a) => this.handleAction(a));
|
updateContractsBoard(container, this.sm.state, (a) => this.handleAction(a));
|
||||||
} else if (tab === 'boardroom') {
|
} else if (tab === 'boardroom') {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Production - The Idle Game: Core Simulation Engine
|
import { PRODUCTS, SECTORS, getUnitMilestoneMultiplier } from '../data/products.js';
|
||||||
|
|
||||||
import { PRODUCTS, getUnitMilestoneMultiplier } from '../data/products.js';
|
|
||||||
import { CONTRACT_TEMPLATES } from '../data/contracts.js';
|
import { CONTRACT_TEMPLATES } from '../data/contracts.js';
|
||||||
import { ACHIEVEMENTS } from '../data/achievements.js';
|
import { ACHIEVEMENTS } from '../data/achievements.js';
|
||||||
|
|
||||||
|
|
@ -10,6 +8,18 @@ export class SimulationEngine {
|
||||||
this.cashRate = 0.0;
|
this.cashRate = 0.0;
|
||||||
this._rateAccumulator = 0.0;
|
this._rateAccumulator = 0.0;
|
||||||
this._rateTimer = 0.0;
|
this._rateTimer = 0.0;
|
||||||
|
this.sectorStats = {};
|
||||||
|
this._sectorAccumulators = {};
|
||||||
|
for (const s of SECTORS) {
|
||||||
|
this.sectorStats[s.id] = {
|
||||||
|
revenueRate: 0.0,
|
||||||
|
unitsRate: 0.0,
|
||||||
|
totalProduced: 0,
|
||||||
|
activeLines: 0,
|
||||||
|
bottlenecks: 0
|
||||||
|
};
|
||||||
|
this._sectorAccumulators[s.id] = { rev: 0.0, units: 0.0 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,6 +62,9 @@ export class SimulationEngine {
|
||||||
pState.stock += produced;
|
pState.stock += produced;
|
||||||
pState.totalProduced = (pState.totalProduced || 0) + produced;
|
pState.totalProduced = (pState.totalProduced || 0) + produced;
|
||||||
state.stats.totalProduced = (state.stats.totalProduced || 0) + produced;
|
state.stats.totalProduced = (state.stats.totalProduced || 0) + produced;
|
||||||
|
if (this._sectorAccumulators[p.sector]) {
|
||||||
|
this._sectorAccumulators[p.sector].units += produced;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if we have required input stock
|
// Check if we have required input stock
|
||||||
|
|
@ -90,6 +103,9 @@ export class SimulationEngine {
|
||||||
pState.stock += produced;
|
pState.stock += produced;
|
||||||
pState.totalProduced = (pState.totalProduced || 0) + produced;
|
pState.totalProduced = (pState.totalProduced || 0) + produced;
|
||||||
state.stats.totalProduced = (state.stats.totalProduced || 0) + produced;
|
state.stats.totalProduced = (state.stats.totalProduced || 0) + produced;
|
||||||
|
if (this._sectorAccumulators[p.sector]) {
|
||||||
|
this._sectorAccumulators[p.sector].units += produced;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +124,9 @@ export class SimulationEngine {
|
||||||
state.cash += earned;
|
state.cash += earned;
|
||||||
state.lifetimeRevenue += earned;
|
state.lifetimeRevenue += earned;
|
||||||
tickRevenue += earned;
|
tickRevenue += earned;
|
||||||
|
if (this._sectorAccumulators[p.sector]) {
|
||||||
|
this._sectorAccumulators[p.sector].rev += earned;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,6 +136,35 @@ export class SimulationEngine {
|
||||||
if (this._rateTimer >= 0.5) {
|
if (this._rateTimer >= 0.5) {
|
||||||
this.cashRate = this._rateAccumulator / this._rateTimer;
|
this.cashRate = this._rateAccumulator / this._rateTimer;
|
||||||
this._rateAccumulator = 0.0;
|
this._rateAccumulator = 0.0;
|
||||||
|
|
||||||
|
for (const s of SECTORS) {
|
||||||
|
const acc = this._sectorAccumulators[s.id] || { rev: 0, units: 0 };
|
||||||
|
const sec = this.sectorStats[s.id];
|
||||||
|
if (sec) {
|
||||||
|
sec.revenueRate = acc.rev / this._rateTimer;
|
||||||
|
sec.unitsRate = acc.units / this._rateTimer;
|
||||||
|
acc.rev = 0.0;
|
||||||
|
acc.units = 0.0;
|
||||||
|
|
||||||
|
let totalProd = 0;
|
||||||
|
let activeLines = 0;
|
||||||
|
let bottlenecks = 0;
|
||||||
|
for (const prod of PRODUCTS) {
|
||||||
|
if (prod.sector === s.id) {
|
||||||
|
const ps = state.products[prod.id];
|
||||||
|
if (ps && ps.unlocked) {
|
||||||
|
totalProd += ps.totalProduced || 0;
|
||||||
|
activeLines += ps.lines || 0;
|
||||||
|
if (ps.isBottlenecked) bottlenecks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sec.totalProduced = totalProd;
|
||||||
|
sec.activeLines = activeLines;
|
||||||
|
sec.bottlenecks = bottlenecks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._rateTimer = 0.0;
|
this._rateTimer = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Production - The Idle Game: Production Floor View
|
// Production - The Idle Game: Production Floor View
|
||||||
// High-performance fine-grained DOM mounting & updating without innerHTML teardowns
|
// High-performance fine-grained DOM mounting & updating with comprehensive sector output metrics
|
||||||
|
|
||||||
import { PRODUCTS, SECTORS, getProduct, getNextMilestone, getUnitMilestoneMultiplier } from '../data/products.js';
|
import { PRODUCTS, SECTORS, getProduct, getNextMilestone, getUnitMilestoneMultiplier } from '../data/products.js';
|
||||||
import { fmtCurrency, fmtNum } from '../core/formatters.js';
|
import { fmtCurrency, fmtNum } from '../core/formatters.js';
|
||||||
|
|
@ -28,9 +28,10 @@ export function calculateMultiCost(baseCost, currentCount, multiplier) {
|
||||||
/**
|
/**
|
||||||
* Mounts the production floor DOM structure ONCE
|
* Mounts the production floor DOM structure ONCE
|
||||||
*/
|
*/
|
||||||
export function mountProductionFloor(container, state, onAction) {
|
export function mountProductionFloor(container, state, onAction, engine) {
|
||||||
const selectedSector = state.selectedSector || 'all';
|
const selectedSector = state.selectedSector || 'all';
|
||||||
const buyMode = state.buyMode || '1';
|
const buyMode = state.buyMode || '1';
|
||||||
|
const currentSectorObj = SECTORS.find(s => s.id === selectedSector);
|
||||||
|
|
||||||
const filteredProducts = PRODUCTS.filter(p => {
|
const filteredProducts = PRODUCTS.filter(p => {
|
||||||
if (selectedSector === 'all') return true;
|
if (selectedSector === 'all') return true;
|
||||||
|
|
@ -44,17 +45,92 @@ export function mountProductionFloor(container, state, onAction) {
|
||||||
<iconify-icon icon="lucide:layout-grid"></iconify-icon>
|
<iconify-icon icon="lucide:layout-grid"></iconify-icon>
|
||||||
<span>All Sectors</span>
|
<span>All Sectors</span>
|
||||||
</button>
|
</button>
|
||||||
${SECTORS.map(s => `
|
${SECTORS.map(s => {
|
||||||
|
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 };
|
||||||
|
return `
|
||||||
<button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}">
|
<button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}">
|
||||||
<iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon>
|
<iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon>
|
||||||
<span>${s.name}</span>
|
<span>${s.name}</span>
|
||||||
|
<span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 2)}/s</span>
|
||||||
</button>
|
</button>
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="production-grid">
|
|
||||||
`;
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 1. If a specific sector is selected: Render the Sector Executive Summary Banner
|
||||||
|
if (selectedSector !== 'all' && currentSectorObj) {
|
||||||
|
const stats = engine?.sectorStats?.[selectedSector] || { revenueRate: 0, unitsRate: 0, activeLines: 0, totalProduced: 0 };
|
||||||
|
html += `
|
||||||
|
<div class="sector-summary-banner" style="border-left: 4px solid ${currentSectorObj.color};">
|
||||||
|
<div class="sector-summary-main">
|
||||||
|
<div class="sector-summary-icon" style="background: ${currentSectorObj.color}15; color: ${currentSectorObj.color};">
|
||||||
|
<iconify-icon icon="${currentSectorObj.icon}" class="icon-lg"></iconify-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="sector-summary-badge" style="color: ${currentSectorObj.color};">INDUSTRY OUTPUT OVERVIEW</div>
|
||||||
|
<h2 class="sector-summary-title">${currentSectorObj.name}</h2>
|
||||||
|
<p class="sector-summary-desc">${currentSectorObj.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sector-metrics-strip">
|
||||||
|
<div class="sector-metric-cell">
|
||||||
|
<span class="sec-metric-label">Wholesale Output Rate:</span>
|
||||||
|
<strong class="sec-metric-val font-mono text-profit" id="sec-sum-rev">+$${fmtNum(stats.revenueRate, 2)}/s</strong>
|
||||||
|
</div>
|
||||||
|
<div class="sector-metric-cell">
|
||||||
|
<span class="sec-metric-label">Production Velocity:</span>
|
||||||
|
<strong class="sec-metric-val font-mono" id="sec-sum-units">${fmtNum(stats.unitsRate, 1)} units/s</strong>
|
||||||
|
</div>
|
||||||
|
<div class="sector-metric-cell">
|
||||||
|
<span class="sec-metric-label">Operating Lines:</span>
|
||||||
|
<strong class="sec-metric-val font-mono" id="sec-sum-lines">${stats.activeLines} active</strong>
|
||||||
|
</div>
|
||||||
|
<div class="sector-metric-cell">
|
||||||
|
<span class="sec-metric-label">Lifetime Sector Output:</span>
|
||||||
|
<strong class="sec-metric-val font-mono" id="sec-sum-total">${fmtNum(stats.totalProduced, 0)} units</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If 'All Sectors' is selected: Render the Industry Output Grid summarizing all sectors
|
||||||
|
if (selectedSector === 'all') {
|
||||||
|
html += `
|
||||||
|
<div class="all-sectors-summary-grid">
|
||||||
|
${SECTORS.map(s => {
|
||||||
|
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0, unitsRate: 0, activeLines: 0 };
|
||||||
|
return `
|
||||||
|
<div class="sector-mini-card" data-sector="${s.id}" style="border-top: 3px solid ${s.color};">
|
||||||
|
<div class="mini-card-header">
|
||||||
|
<div class="mini-card-icon" style="background: ${s.color}15; color: ${s.color};">
|
||||||
|
<iconify-icon icon="${s.icon}"></iconify-icon>
|
||||||
|
</div>
|
||||||
|
<div class="mini-card-title-wrap">
|
||||||
|
<h4 class="mini-card-name">${s.name}</h4>
|
||||||
|
<span class="mini-card-sub" id="all-sec-lines-${s.id}">${stats.activeLines} lines active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini-card-stats">
|
||||||
|
<div class="mini-stat-cell">
|
||||||
|
<span class="mini-stat-label">Output Rate</span>
|
||||||
|
<strong class="font-mono text-profit" id="all-sec-rev-${s.id}">+$${fmtNum(stats.revenueRate, 2)}/s</strong>
|
||||||
|
</div>
|
||||||
|
<div class="mini-stat-cell">
|
||||||
|
<span class="mini-stat-label">Velocity</span>
|
||||||
|
<strong class="font-mono" id="all-sec-units-${s.id}">${fmtNum(stats.unitsRate, 1)} u/s</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<div class="production-grid">`;
|
||||||
|
|
||||||
for (const p of filteredProducts) {
|
for (const p of filteredProducts) {
|
||||||
const pState = state.products[p.id] || { unlocked: false, lines: 0, stock: 0, cycleProgress: 0, isBottlenecked: false };
|
const pState = state.products[p.id] || { unlocked: false, lines: 0, stock: 0, cycleProgress: 0, isBottlenecked: false };
|
||||||
|
|
@ -213,6 +289,10 @@ export function mountProductionFloor(container, state, onAction) {
|
||||||
btn.onclick = () => onAction({ type: 'SELECT_SECTOR', sector: btn.dataset.sector });
|
btn.onclick = () => onAction({ type: 'SELECT_SECTOR', sector: btn.dataset.sector });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
container.querySelectorAll('.sector-mini-card').forEach(card => {
|
||||||
|
card.onclick = () => onAction({ type: 'SELECT_SECTOR', sector: card.dataset.sector });
|
||||||
|
});
|
||||||
|
|
||||||
container.querySelectorAll('.facility-card').forEach(card => {
|
container.querySelectorAll('.facility-card').forEach(card => {
|
||||||
card.onclick = (e) => {
|
card.onclick = (e) => {
|
||||||
if (e.target.closest('button')) return;
|
if (e.target.closest('button')) return;
|
||||||
|
|
@ -244,15 +324,73 @@ export function mountProductionFloor(container, state, onAction) {
|
||||||
/**
|
/**
|
||||||
* Fine-grained in-place DOM updates (0% layout thrashing, 0% DOM teardown)
|
* Fine-grained in-place DOM updates (0% layout thrashing, 0% DOM teardown)
|
||||||
*/
|
*/
|
||||||
export function updateProductionFloor(container, state) {
|
export function updateProductionFloor(container, state, engine) {
|
||||||
const buyMode = state.buyMode || '1';
|
const buyMode = state.buyMode || '1';
|
||||||
const cash = state.cash;
|
const cash = state.cash;
|
||||||
|
const selectedSector = state.selectedSector || 'all';
|
||||||
|
|
||||||
|
// 1. Update Sector Navigation Pills Output Rates
|
||||||
|
for (const s of SECTORS) {
|
||||||
|
const stats = engine?.sectorStats?.[s.id];
|
||||||
|
if (stats) {
|
||||||
|
const elPillRate = document.getElementById(`pill-rate-${s.id}`);
|
||||||
|
if (elPillRate) {
|
||||||
|
const text = `+$${fmtNum(stats.revenueRate, 2)}/s`;
|
||||||
|
if (elPillRate.textContent !== text) elPillRate.textContent = text;
|
||||||
|
}
|
||||||
|
// Update All-Sectors Mini Cards if on 'all'
|
||||||
|
if (selectedSector === 'all') {
|
||||||
|
const elMiniRev = document.getElementById(`all-sec-rev-${s.id}`);
|
||||||
|
if (elMiniRev) {
|
||||||
|
const text = `+$${fmtNum(stats.revenueRate, 2)}/s`;
|
||||||
|
if (elMiniRev.textContent !== text) elMiniRev.textContent = text;
|
||||||
|
}
|
||||||
|
const elMiniUnits = document.getElementById(`all-sec-units-${s.id}`);
|
||||||
|
if (elMiniUnits) {
|
||||||
|
const text = `${fmtNum(stats.unitsRate, 1)} u/s`;
|
||||||
|
if (elMiniUnits.textContent !== text) elMiniUnits.textContent = text;
|
||||||
|
}
|
||||||
|
const elMiniLines = document.getElementById(`all-sec-lines-${s.id}`);
|
||||||
|
if (elMiniLines) {
|
||||||
|
const text = `${stats.activeLines} lines active`;
|
||||||
|
if (elMiniLines.textContent !== text) elMiniLines.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update Sector Summary Banner if viewing a single sector
|
||||||
|
if (selectedSector !== 'all') {
|
||||||
|
const stats = engine?.sectorStats?.[selectedSector];
|
||||||
|
if (stats) {
|
||||||
|
const elRev = document.getElementById('sec-sum-rev');
|
||||||
|
if (elRev) {
|
||||||
|
const text = `+$${fmtNum(stats.revenueRate, 2)}/s`;
|
||||||
|
if (elRev.textContent !== text) elRev.textContent = text;
|
||||||
|
}
|
||||||
|
const elUnits = document.getElementById('sec-sum-units');
|
||||||
|
if (elUnits) {
|
||||||
|
const text = `${fmtNum(stats.unitsRate, 1)} units/s`;
|
||||||
|
if (elUnits.textContent !== text) elUnits.textContent = text;
|
||||||
|
}
|
||||||
|
const elLines = document.getElementById('sec-sum-lines');
|
||||||
|
if (elLines) {
|
||||||
|
const text = `${stats.activeLines} active`;
|
||||||
|
if (elLines.textContent !== text) elLines.textContent = text;
|
||||||
|
}
|
||||||
|
const elTotal = document.getElementById('sec-sum-total');
|
||||||
|
if (elTotal) {
|
||||||
|
const text = `${fmtNum(stats.totalProduced, 0)} units`;
|
||||||
|
if (elTotal.textContent !== text) elTotal.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update Facility Cards
|
||||||
for (const p of PRODUCTS) {
|
for (const p of PRODUCTS) {
|
||||||
const pState = state.products[p.id];
|
const pState = state.products[p.id];
|
||||||
if (!pState) continue;
|
if (!pState) continue;
|
||||||
|
|
||||||
// If card is locked: only update unlock button disabled state
|
|
||||||
if (!pState.unlocked) {
|
if (!pState.unlocked) {
|
||||||
const btnUnlock = document.getElementById(`btn-unlock-${p.id}`);
|
const btnUnlock = document.getElementById(`btn-unlock-${p.id}`);
|
||||||
if (btnUnlock) {
|
if (btnUnlock) {
|
||||||
|
|
@ -264,14 +402,14 @@ export function updateProductionFloor(container, state) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Progress Bar (smooth width update)
|
// Progress Bar
|
||||||
const elProgress = document.getElementById(`progress-${p.id}`);
|
const elProgress = document.getElementById(`progress-${p.id}`);
|
||||||
if (elProgress) {
|
if (elProgress) {
|
||||||
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
||||||
elProgress.style.width = `${progressPct}%`;
|
elProgress.style.width = `${progressPct}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Stockpile Text
|
// Stockpile Text
|
||||||
const elStock = document.getElementById(`stock-${p.id}`);
|
const elStock = document.getElementById(`stock-${p.id}`);
|
||||||
if (elStock) {
|
if (elStock) {
|
||||||
const stockText = fmtNum(pState.stock, 0);
|
const stockText = fmtNum(pState.stock, 0);
|
||||||
|
|
@ -280,7 +418,7 @@ export function updateProductionFloor(container, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Status Badge (only update if bottlenecked state changed)
|
// Status Badge
|
||||||
const elBadge = document.getElementById(`badge-${p.id}`);
|
const elBadge = document.getElementById(`badge-${p.id}`);
|
||||||
if (elBadge) {
|
if (elBadge) {
|
||||||
const isBottlenecked = pState.isBottlenecked;
|
const isBottlenecked = pState.isBottlenecked;
|
||||||
|
|
@ -295,7 +433,7 @@ export function updateProductionFloor(container, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Input Chips Stock
|
// Input Chips Stock
|
||||||
if (p.inputs && p.inputs.length > 0) {
|
if (p.inputs && p.inputs.length > 0) {
|
||||||
for (const inp of p.inputs) {
|
for (const inp of p.inputs) {
|
||||||
const currentStock = state.products[inp.id]?.stock || 0;
|
const currentStock = state.products[inp.id]?.stock || 0;
|
||||||
|
|
@ -315,7 +453,7 @@ export function updateProductionFloor(container, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Expand Button disabled state
|
// Expand Button disabled state
|
||||||
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
|
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
|
||||||
const elBtnExpand = document.getElementById(`btn-expand-${p.id}`);
|
const elBtnExpand = document.getElementById(`btn-expand-${p.id}`);
|
||||||
if (elBtnExpand) {
|
if (elBtnExpand) {
|
||||||
|
|
@ -325,7 +463,7 @@ export function updateProductionFloor(container, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Hire Manager disabled state
|
// Hire Manager disabled state
|
||||||
const elBtnHire = document.getElementById(`btn-hire-${p.id}`);
|
const elBtnHire = document.getElementById(`btn-hire-${p.id}`);
|
||||||
if (elBtnHire) {
|
if (elBtnHire) {
|
||||||
const canAfford = cash >= p.baseManagerCost;
|
const canAfford = cash >= p.baseManagerCost;
|
||||||
|
|
@ -334,7 +472,7 @@ export function updateProductionFloor(container, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Logistics Button disabled state
|
// Logistics Button disabled state
|
||||||
const logisticsCost = p.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0);
|
const logisticsCost = p.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0);
|
||||||
const elBtnLogistics = document.getElementById(`btn-logistics-${p.id}`);
|
const elBtnLogistics = document.getElementById(`btn-logistics-${p.id}`);
|
||||||
if (elBtnLogistics) {
|
if (elBtnLogistics) {
|
||||||
|
|
@ -344,7 +482,7 @@ export function updateProductionFloor(container, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Selected Card class
|
// Selected Card class
|
||||||
const elCard = document.getElementById(`card-${p.id}`);
|
const elCard = document.getElementById(`card-${p.id}`);
|
||||||
if (elCard) {
|
if (elCard) {
|
||||||
const isSelected = state.selectedProductId === p.id;
|
const isSelected = state.selectedProductId === p.id;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue