From 67e13a40b1e2f40f6c8749545ff8efc72ae6e606 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 15 Oct 2026 22:36:51 +0200 Subject: [PATCH] feat(sectors): add total output metrics per industry across sidebar, tabs, summary banner, and all-sectors grid --- frontend/css/style.css | 201 ++++++++++++++++++++++++++++++- frontend/index.html | 66 ++++++---- frontend/js/app.js | 34 +++++- frontend/js/core/engine.js | 54 ++++++++- frontend/js/ui/productionView.js | 178 ++++++++++++++++++++++++--- 5 files changed, 478 insertions(+), 55 deletions(-) diff --git a/frontend/css/style.css b/frontend/css/style.css index 877adc0..b831cd5 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -346,19 +346,212 @@ body { 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; align-items: center; gap: 8px; - padding: 6px 10px; font-size: 0.78rem; 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); + 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); } -.sidebar-sector-item iconify-icon { - font-size: 1rem; +.mini-stat-cell { + 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 */ diff --git a/frontend/index.html b/frontend/index.html index 97de587..393af59 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -112,30 +112,48 @@ diff --git a/frontend/js/app.js b/frontend/js/app.js index 04188fa..b250fa6 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -4,8 +4,7 @@ import { GState } from './core/state.js'; import { SimulationEngine } from './core/engine.js'; import { SyncManager } from './core/sync.js'; -import { fmtCurrency, fmtNum, fmtRate, getNotation, setNotation } from './core/formatters.js'; -import { PRODUCTS, getProduct } from './data/products.js'; +import { PRODUCTS, SECTORS, getProduct } from './data/products.js'; import { mountProductionFloor, updateProductionFloor, calculateMultiCost } from './ui/productionView.js'; import { mountContractsBoard, updateContractsBoard } from './ui/contractsView.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 const userBadge = document.getElementById('user-badge'); if (userBadge) { @@ -338,6 +350,20 @@ class ProductionApp { if (userBadge) { 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; 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') { mountContractsBoard(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'boardroom') { @@ -388,7 +414,7 @@ class ProductionApp { } if (tab === 'production') { - updateProductionFloor(container, this.sm.state); + updateProductionFloor(container, this.sm.state, this.engine); } else if (tab === 'contracts') { updateContractsBoard(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'boardroom') { diff --git a/frontend/js/core/engine.js b/frontend/js/core/engine.js index 0399d0a..f761251 100644 --- a/frontend/js/core/engine.js +++ b/frontend/js/core/engine.js @@ -1,6 +1,4 @@ -// Production - The Idle Game: Core Simulation Engine - -import { PRODUCTS, getUnitMilestoneMultiplier } from '../data/products.js'; +import { PRODUCTS, SECTORS, getUnitMilestoneMultiplier } from '../data/products.js'; import { CONTRACT_TEMPLATES } from '../data/contracts.js'; import { ACHIEVEMENTS } from '../data/achievements.js'; @@ -10,6 +8,18 @@ export class SimulationEngine { this.cashRate = 0.0; this._rateAccumulator = 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.totalProduced = (pState.totalProduced || 0) + produced; state.stats.totalProduced = (state.stats.totalProduced || 0) + produced; + if (this._sectorAccumulators[p.sector]) { + this._sectorAccumulators[p.sector].units += produced; + } } } else { // Check if we have required input stock @@ -90,6 +103,9 @@ export class SimulationEngine { pState.stock += produced; pState.totalProduced = (pState.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.lifetimeRevenue += 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) { this.cashRate = this._rateAccumulator / this._rateTimer; 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; } diff --git a/frontend/js/ui/productionView.js b/frontend/js/ui/productionView.js index 7167910..0fd0544 100644 --- a/frontend/js/ui/productionView.js +++ b/frontend/js/ui/productionView.js @@ -1,5 +1,5 @@ // 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 { fmtCurrency, fmtNum } from '../core/formatters.js'; @@ -28,9 +28,10 @@ export function calculateMultiCost(baseCost, currentCount, multiplier) { /** * 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 buyMode = state.buyMode || '1'; + const currentSectorObj = SECTORS.find(s => s.id === selectedSector); const filteredProducts = PRODUCTS.filter(p => { if (selectedSector === 'all') return true; @@ -44,18 +45,93 @@ export function mountProductionFloor(container, state, onAction) { All Sectors - ${SECTORS.map(s => ` - - `).join('')} + ${SECTORS.map(s => { + const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 }; + return ` + + `; + }).join('')} - -
`; + // 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 += ` +
+
+
+ +
+
+
INDUSTRY OUTPUT OVERVIEW
+

${currentSectorObj.name}

+

${currentSectorObj.description}

+
+
+
+
+ Wholesale Output Rate: + +$${fmtNum(stats.revenueRate, 2)}/s +
+
+ Production Velocity: + ${fmtNum(stats.unitsRate, 1)} units/s +
+
+ Operating Lines: + ${stats.activeLines} active +
+
+ Lifetime Sector Output: + ${fmtNum(stats.totalProduced, 0)} units +
+
+
+ `; + } + + // 2. If 'All Sectors' is selected: Render the Industry Output Grid summarizing all sectors + if (selectedSector === 'all') { + html += ` +
+ ${SECTORS.map(s => { + const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0, unitsRate: 0, activeLines: 0 }; + return ` +
+
+
+ +
+
+

${s.name}

+ ${stats.activeLines} lines active +
+
+
+
+ Output Rate + +$${fmtNum(stats.revenueRate, 2)}/s +
+
+ Velocity + ${fmtNum(stats.unitsRate, 1)} u/s +
+
+
+ `; + }).join('')} +
+ `; + } + + html += `
`; + 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); @@ -213,6 +289,10 @@ export function mountProductionFloor(container, state, onAction) { 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 => { card.onclick = (e) => { 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) */ -export function updateProductionFloor(container, state) { +export function updateProductionFloor(container, state, engine) { const buyMode = state.buyMode || '1'; 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) { const pState = state.products[p.id]; if (!pState) continue; - // If card is locked: only update unlock button disabled state if (!pState.unlocked) { const btnUnlock = document.getElementById(`btn-unlock-${p.id}`); if (btnUnlock) { @@ -264,14 +402,14 @@ export function updateProductionFloor(container, state) { continue; } - // 1. Progress Bar (smooth width update) + // Progress Bar const elProgress = document.getElementById(`progress-${p.id}`); if (elProgress) { const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100)); elProgress.style.width = `${progressPct}%`; } - // 2. Stockpile Text + // Stockpile Text const elStock = document.getElementById(`stock-${p.id}`); if (elStock) { 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}`); if (elBadge) { 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) { for (const inp of p.inputs) { 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 elBtnExpand = document.getElementById(`btn-expand-${p.id}`); 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}`); if (elBtnHire) { 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 elBtnLogistics = document.getElementById(`btn-logistics-${p.id}`); 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}`); if (elCard) { const isSelected = state.selectedProductId === p.id;