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) {