Refactor large files into modular architecture: split CSS, products, and production floor UI

This commit is contained in:
Christian 2026-10-16 01:22:02 +02:00
parent 032b0cc255
commit f94d7df8f8
21 changed files with 3065 additions and 2966 deletions

View file

@ -0,0 +1,62 @@
/* Production - The Idle Game: Enterprise Design System & Layout */
:root {
--bg-app: #f8fafc;
--bg-surface: #ffffff;
--bg-surface-sub: #f1f5f9;
--bg-header: rgba(255, 255, 255, 0.95);
--border-color: #e2e8f0;
--border-subtle: #cbd5e1;
--border-focus: #3b82f6;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--primary-blue: #2563eb;
--primary-blue-hover: #1d4ed8;
--primary-blue-light: #eff6ff;
--emerald-profit: #10b981;
--emerald-dark: #059669;
--emerald-light: #ecfdf5;
--amber-warning: #f59e0b;
--amber-dark: #d97706;
--amber-light: #fffbeb;
--purple-rnd: #7c3aed;
--purple-light: #f5f3ff;
--font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.03);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
body {
background-color: var(--bg-app);
color: var(--text-primary);
font-family: var(--font-sans);
min-height: 100vh;
overflow-x: hidden;
}
.font-mono {
font-family: var(--font-mono);
}

View file

@ -0,0 +1,193 @@
/* ==========================================================================
EXECUTIVE STICKY HEADER & FINANCIAL HUD
========================================================================== */
#executive-header {
position: sticky;
top: 0;
z-index: 100;
background: var(--bg-header);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
#corporate-status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
border-bottom: 1px solid var(--border-color);
}
.corporate-brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo-icon {
width: 32px;
height: 32px;
background: var(--primary-blue-light);
color: var(--primary-blue);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
}
.brand-title {
font-size: 1rem;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--text-primary);
}
.brand-subtitle {
font-size: 0.72rem;
font-weight: 600;
color: var(--primary-blue);
margin-left: 6px;
background: var(--primary-blue-light);
padding: 2px 6px;
border-radius: 4px;
}
.status-right {
display: flex;
align-items: center;
gap: 8px;
}
.btn-icon-soft {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
color: var(--text-secondary);
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-icon-soft:hover {
background: var(--border-color);
color: var(--text-primary);
}
.btn-user-badge {
background: none;
border: 1px solid var(--border-color);
color: var(--primary-blue);
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
font-family: var(--font-mono);
}
.btn-user-badge:hover {
background: var(--primary-blue-light);
}
/* Financial HUD Grid */
#financial-hud {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
padding: 10px 16px;
align-items: center;
}
.hud-stat-box {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 8px 12px;
display: flex;
flex-direction: column;
}
.primary-hud-box {
border-color: #bfdbfe;
background: #f8faff;
}
.hud-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.hud-main-val {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
margin: 2px 0;
}
.hud-sub-val {
font-size: 0.75rem;
font-weight: 600;
}
.rate-positive {
color: var(--emerald-dark);
}
.text-equity {
color: var(--purple-rnd);
}
.hud-multiplier-box {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
}
.multi-label {
font-size: 0.72rem;
font-weight: 700;
color: var(--text-secondary);
}
.multi-button-group {
display: flex;
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 2px;
gap: 2px;
}
.btn-multi-pill {
background: transparent;
border: none;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
color: var(--text-secondary);
cursor: pointer;
font-family: var(--font-mono);
transition: all 0.15s;
}
.btn-multi-pill.active {
background: var(--primary-blue);
color: #ffffff;
box-shadow: var(--shadow-sm);
}

View file

@ -0,0 +1,234 @@
/* ==========================================================================
MOBILE BOTTOM NAVIGATION & RESPONSIVE RULES
========================================================================== */
#mobile-bottom-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: var(--bg-header);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-top: 1px solid var(--border-color);
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.04);
z-index: 100;
justify-content: space-around;
align-items: center;
}
.nav-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 0.7rem;
font-weight: 600;
cursor: pointer;
flex: 1;
padding: 8px 0;
}
.nav-tab iconify-icon {
font-size: 1.3rem;
}
.nav-tab.active {
color: var(--primary-blue);
font-weight: 700;
}
/* Responsive Breakpoints */
@media (max-width: 1023px) {
#desktop-nav-sidebar {
display: none;
}
#desktop-inspector-panel {
display: none;
}
#mobile-bottom-nav {
display: flex;
}
#workspace-layout {
padding-bottom: 80px;
}
.production-grid, .contracts-grid, .perks-grid, .achievements-grid {
grid-template-columns: 1fr;
}
}
/* ==========================================================================
TOASTS & MODALS
========================================================================== */
#toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
@media (max-width: 1023px) {
#toast-container {
bottom: 74px;
left: 20px;
right: 20px;
}
}
.toast-msg {
background: #1e293b;
color: #ffffff;
padding: 10px 16px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: 10px;
font-size: 0.82rem;
font-weight: 600;
animation: slideInToast 0.25s ease-out;
}
.toast-msg.toast-success {
border-left: 4px solid var(--emerald-profit);
}
.toast-msg.toast-warning {
border-left: 4px solid var(--amber-dark);
}
.toast-msg.toast-info {
border-left: 4px solid var(--primary-blue);
}
.toast-msg.fade-out {
opacity: 0;
transform: translateY(10px);
transition: all 0.3s;
}
@keyframes slideInToast {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(4px);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 16px;
}
.modal-overlay.open {
display: flex;
}
.modal-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 24px;
max-width: 420px;
width: 100%;
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-header {
display: flex;
align-items: center;
gap: 12px;
}
.modal-icon {
font-size: 2rem;
color: var(--primary-blue);
}
.modal-title {
font-size: 1.15rem;
font-weight: 800;
}
.modal-subtitle {
font-size: 0.78rem;
color: var(--text-secondary);
}
.input-field-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.input-field-group label {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-secondary);
}
.modal-input {
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 0.88rem;
font-family: var(--font-sans);
outline: none;
}
.modal-input:focus {
border-color: var(--primary-blue);
}
.modal-actions-row {
display: flex;
gap: 8px;
}
.modal-actions-row button {
flex: 1;
padding: 10px;
}
.btn-text-close {
background: none;
border: none;
font-size: 0.78rem;
color: var(--text-muted);
cursor: pointer;
text-align: center;
}
.btn-text-close:hover {
color: var(--text-primary);
}

View file

@ -0,0 +1,318 @@
/* ==========================================================================
WORKSPACE 3-COLUMN LAYOUT (DESKTOP)
========================================================================== */
#workspace-layout {
display: flex;
width: 100%;
max-width: 1680px;
margin: 0 auto;
padding: 16px;
gap: 16px;
min-height: calc(100vh - 120px);
}
/* Left Sidebar */
#desktop-nav-sidebar {
width: 230px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-section-title {
font-size: 0.72rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0 4px;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-md);
color: var(--text-secondary);
font-size: 0.88rem;
font-weight: 600;
cursor: pointer;
text-align: left;
transition: all 0.15s;
}
.nav-btn iconify-icon {
font-size: 1.2rem;
}
.nav-btn:hover {
background: var(--bg-surface);
color: var(--text-primary);
border-color: var(--border-color);
}
.nav-btn.active {
background: var(--primary-blue-light);
color: var(--primary-blue);
border-color: #bfdbfe;
font-weight: 700;
}
.nav-pill-badge {
margin-left: auto;
background: var(--amber-dark);
color: #ffffff;
font-size: 0.7rem;
font-weight: 700;
padding: 2px 6px;
border-radius: 10px;
}
.sidebar-divider {
height: 1px;
background: var(--border-color);
margin: 4px 0;
}
.sidebar-sectors-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.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;
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.72rem;
font-weight: 700;
color: var(--emerald-dark);
margin-left: 2px;
letter-spacing: -0.02em;
white-space: nowrap;
}
/* 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);
}
.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 */
#main-viewport {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Right Inspector Panel (Desktop) */
#desktop-inspector-panel {
width: 340px;
flex-shrink: 0;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
/* ==========================================================================
ACHIEVEMENTS VIEW
========================================================================== */
.achievements-header-pane {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 16px 20px;
}
.achievements-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 16px;
}
.achievement-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 16px;
display: flex;
gap: 14px;
box-shadow: var(--shadow-sm);
}
.ach-icon-circle {
width: 44px;
height: 44px;
background: var(--bg-surface-sub);
color: var(--text-muted);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
}
.ach-icon-circle.ach-unlocked {
background: #fef3c7;
color: #d97706;
}
.ach-info-col {
flex: 1;
}
.ach-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
.ach-title {
font-size: 0.95rem;
font-weight: 700;
}
.ach-tier-badge {
font-size: 0.7rem;
font-weight: 700;
color: var(--primary-blue);
}
.ach-desc {
font-size: 0.78rem;
color: var(--text-secondary);
margin-bottom: 8px;
}
.ach-progress-bar-track {
width: 100%;
height: 6px;
background: var(--bg-surface-sub);
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.ach-progress-bar-fill {
height: 100%;
background: #f59e0b;
}
.ach-progress-labels {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: var(--text-muted);
}

View file

@ -0,0 +1,213 @@
/* ==========================================================================
BOARDROOM & IPO VIEW
========================================================================== */
.boardroom-header-pane {
display: flex;
flex-direction: column;
gap: 16px;
}
.boardroom-hero {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: #ffffff;
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: var(--shadow-md);
}
.executive-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #93c5fd;
margin-bottom: 6px;
}
.boardroom-title {
font-size: 1.5rem;
font-weight: 800;
}
.boardroom-desc {
font-size: 0.85rem;
color: #94a3b8;
max-width: 600px;
margin-top: 6px;
line-height: 1.5;
}
.hero-valuation-card {
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--radius-md);
padding: 16px 20px;
text-align: right;
}
.val-label {
font-size: 0.72rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.val-number {
font-size: 1.75rem;
font-weight: 800;
color: #ffffff;
margin: 4px 0;
}
.val-lifetime {
font-size: 0.78rem;
color: #cbd5e1;
}
.ipo-action-banner {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 16px 20px;
}
.ipo-banner-info {
display: flex;
align-items: center;
gap: 16px;
}
.ipo-shares-badge {
background: var(--purple-light);
color: var(--purple-rnd);
font-size: 1.1rem;
font-weight: 800;
padding: 10px 16px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
gap: 6px;
}
.boardroom-shares-summary {
display: flex;
gap: 12px;
}
.shares-pill {
background: var(--bg-surface);
border: 1px solid var(--border-color);
padding: 8px 16px;
border-radius: var(--radius-md);
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 8px;
}
.shares-val {
color: var(--purple-rnd);
font-size: 1rem;
}
.perks-section-title {
margin-top: 8px;
}
.perks-section-title h3 {
font-size: 1.15rem;
font-weight: 800;
display: flex;
align-items: center;
gap: 6px;
}
.perks-section-title span {
font-size: 0.8rem;
color: var(--text-secondary);
}
.perks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.perk-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.perk-header {
display: flex;
gap: 12px;
align-items: center;
}
.perk-tier-badge {
font-size: 0.7rem;
font-weight: 700;
color: var(--primary-blue);
}
.perk-name {
font-size: 0.95rem;
font-weight: 700;
color: var(--text-primary);
}
.perk-desc {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.4;
}
.perk-effect-box {
display: flex;
align-items: center;
gap: 6px;
background: var(--bg-surface-sub);
padding: 8px 10px;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
}
.btn-corp-perk {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--purple-rnd);
color: #ffffff;
border: none;
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
}
.btn-corp-perk:disabled {
background: var(--border-subtle);
cursor: not-allowed;
opacity: 0.65;
}

View file

@ -0,0 +1,184 @@
/* ==========================================================================
B2B CONTRACTS BOARD
========================================================================== */
.contracts-header-pane {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 16px 20px;
}
.view-title {
font-size: 1.25rem;
font-weight: 800;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.view-subtitle {
font-size: 0.82rem;
color: var(--text-secondary);
margin-top: 4px;
}
.contracts-meta-badge {
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
padding: 6px 14px;
border-radius: var(--radius-md);
font-size: 0.8rem;
color: var(--text-secondary);
}
.contracts-meta-badge strong {
color: var(--primary-blue);
margin-left: 4px;
}
.contracts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 16px;
}
.contract-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
box-shadow: var(--shadow-sm);
transition: all 0.2s;
}
.contract-card.ready-to-fulfill {
border-color: var(--emerald-profit);
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.2);
}
.contract-client-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.client-brand {
display: flex;
align-items: center;
gap: 10px;
}
.client-avatar {
width: 36px;
height: 36px;
background: var(--primary-blue-light);
color: var(--primary-blue);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
}
.client-name {
font-size: 0.9rem;
font-weight: 700;
color: var(--text-primary);
}
.client-badge {
font-size: 0.7rem;
font-weight: 600;
color: var(--text-muted);
}
.contract-timer {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--bg-surface-sub);
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 700;
color: var(--text-secondary);
}
.timer-urgent {
background: var(--amber-light);
color: var(--amber-dark);
}
.contract-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.contract-req-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 6px;
}
.req-label {
font-size: 0.72rem;
color: var(--text-muted);
}
.req-val-wrap {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
}
.contract-progress-track {
width: 100%;
height: 8px;
background: var(--bg-surface-sub);
border-radius: 4px;
overflow: hidden;
margin-bottom: 12px;
}
.contract-progress-fill {
height: 100%;
background: var(--primary-blue);
transition: width 0.3s;
}
.contract-card.ready-to-fulfill .contract-progress-fill {
background: var(--emerald-profit);
}
.contract-payout-box {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
background: var(--bg-surface-sub);
padding: 10px;
border-radius: var(--radius-md);
}
.payout-label {
font-size: 0.7rem;
color: var(--text-muted);
}
.payout-val {
font-size: 0.95rem;
font-weight: 700;
color: var(--emerald-dark);
}

View file

@ -0,0 +1,207 @@
/* ==========================================================================
DESKTOP INSPECTOR PANEL
========================================================================== */
.inspector-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 18px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 16px;
position: sticky;
top: 150px;
}
.inspector-badge-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.sector-tag {
font-size: 0.72rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 4px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.tier-tag {
font-size: 0.72rem;
font-weight: 700;
color: var(--text-muted);
}
.inspector-title {
font-size: 1.15rem;
font-weight: 800;
color: var(--text-primary);
}
.inspector-desc {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.4;
margin-top: 4px;
}
.inspector-metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
background: var(--bg-surface-sub);
padding: 10px;
border-radius: var(--radius-md);
}
.metric-box {
display: flex;
flex-direction: column;
}
.metric-label {
font-size: 0.68rem;
color: var(--text-muted);
}
.metric-val {
font-size: 0.88rem;
color: var(--text-primary);
}
.inspector-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.section-heading {
font-size: 0.82rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
}
.section-subtext {
font-size: 0.72rem;
color: var(--text-muted);
}
.liquidation-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.liquidation-buttons button {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.78rem;
padding: 8px 12px;
white-space: nowrap;
}
.liquidation-btn-label {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 700;
}
.liquidation-btn-val {
font-family: var(--font-mono);
font-weight: 700;
}
.chain-box {
display: flex;
flex-direction: column;
gap: 6px;
background: #f8fafc;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 10px;
}
.chain-title {
font-size: 0.7rem;
font-weight: 700;
color: var(--text-secondary);
text-transform: uppercase;
}
.chain-empty {
font-size: 0.75rem;
color: var(--text-muted);
font-style: italic;
}
.chain-items {
display: flex;
flex-direction: column;
gap: 6px;
}
.chain-item-row {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-surface);
border: 1px solid var(--border-color);
padding: 8px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.15s;
gap: 10px;
}
.chain-item-row:hover {
background: var(--primary-blue-light);
}
.chain-item-left {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.chain-item-ratio {
font-size: 0.72rem;
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
}
.chain-badge {
font-size: 0.68rem;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
}
.badge-open {
background: var(--emerald-light);
color: var(--emerald-dark);
}
.badge-locked {
background: var(--bg-surface-sub);
color: var(--text-muted);
}

View file

@ -0,0 +1,395 @@
/* ==========================================================================
PRODUCTION FLOOR & FACILITY CARDS
========================================================================== */
.production-toolbar {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 6px 8px;
}
.sector-tabs-scroll {
display: flex;
gap: 6px;
width: 100%;
}
.sector-pill {
flex: 1 1 0;
min-width: 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
padding: 8px 6px;
border-radius: var(--radius-sm);
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
box-sizing: border-box;
}
.sector-pill iconify-icon {
font-size: 1.15rem;
flex-shrink: 0;
}
.sector-pill:hover {
border-color: var(--text-muted);
}
.sector-pill.active {
background: var(--text-primary);
color: #ffffff;
border-color: var(--text-primary);
}
.sector-pill.active iconify-icon {
color: #ffffff !important;
}
.sector-pill.active .sector-pill-rate {
color: #a7f3d0;
}
.production-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 16px;
}
.facility-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 16px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 420px;
gap: 12px;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
position: relative;
box-sizing: border-box;
}
.facility-card:hover {
border-color: #cbd5e1;
box-shadow: var(--shadow-md);
}
.facility-card.selected {
border-color: var(--primary-blue);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
}
.facility-card.locked-card {
opacity: 0.85;
background: #fbfcfe;
}
.facility-header {
display: flex;
gap: 12px;
align-items: flex-start;
min-height: 60px;
box-sizing: border-box;
}
.facility-icon-wrap {
width: 44px;
height: 44px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.icon-lg {
font-size: 1.6rem;
}
.facility-title-wrap {
flex: 1;
min-width: 0;
overflow: hidden;
}
.facility-meta-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
min-height: 22px;
height: 22px;
flex-wrap: nowrap;
margin-bottom: 2px;
box-sizing: border-box;
}
.sector-badge {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.facility-name {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.facility-type-sub {
font-size: 0.75rem;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.facility-desc {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.4;
}
/* Status Badges */
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 12px;
white-space: nowrap;
flex-shrink: 0;
height: 20px;
line-height: 1;
box-sizing: border-box;
}
.badge-success {
background: var(--emerald-light);
color: var(--emerald-dark);
}
.badge-warning {
background: var(--amber-light);
color: var(--amber-dark);
animation: pulseWarning 1.5s infinite;
}
@keyframes pulseWarning {
0%, 100% { opacity: 1; }
50% { opacity: 0.75; }
}
.badge-idle {
background: var(--bg-surface-sub);
color: var(--text-muted);
}
/* Cycle Progress Track */
.cycle-bar-track {
width: 100%;
height: 6px;
background: var(--bg-surface-sub);
border-radius: 3px;
overflow: hidden;
}
.cycle-bar-fill {
height: 100%;
transition: width 0.1s linear;
}
/* Facility Stats Grid */
.facility-stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
background: var(--bg-surface-sub);
border-radius: var(--radius-sm);
padding: 8px 10px;
min-height: 84px;
box-sizing: border-box;
}
.stat-cell {
display: flex;
flex-direction: column;
min-width: 0;
}
.stat-label {
font-size: 0.65rem;
font-weight: 600;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-value {
font-size: 0.78rem;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-warning {
color: var(--amber-dark);
}
/* Inputs Strip */
.inputs-strip {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 8px;
background: #f8fafc;
border: 1px dashed var(--border-color);
border-radius: var(--radius-sm);
}
.inputs-label {
font-size: 0.68rem;
font-weight: 700;
color: var(--text-secondary);
}
.inputs-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.input-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.72rem;
background: var(--bg-surface);
border: 1px solid var(--border-color);
padding: 2px 6px;
border-radius: 4px;
color: var(--text-secondary);
}
.input-chip.low-stock {
border-color: var(--amber-dark);
background: var(--amber-light);
color: var(--amber-dark);
}
/* Action Buttons */
.facility-actions-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: auto;
}
.btn-primary-corp {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--primary-blue);
color: #ffffff;
border: none;
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, opacity 0.15s;
}
.btn-primary-corp:hover:not(:disabled) {
background: var(--primary-blue-hover);
}
.btn-primary-corp:disabled {
background: var(--border-subtle);
cursor: not-allowed;
opacity: 0.65;
}
.btn-corp-action {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
background: var(--emerald-dark);
color: #ffffff;
border: none;
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
}
.btn-corp-action:hover {
background: var(--emerald-profit);
}
.btn-corp-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 7px 10px;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
cursor: pointer;
white-space: nowrap;
}
.btn-corp-secondary:hover:not(:disabled) {
background: var(--bg-surface-sub);
}
.btn-corp-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.mgr-hired-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--primary-blue-light);
color: var(--primary-blue);
font-size: 0.72rem;
font-weight: 700;
padding: 6px 10px;
border-radius: var(--radius-sm);
white-space: nowrap;
}

View file

@ -1,569 +1,29 @@
// Production - The Idle Game: Product Catalog & Supply Chain Definitions // Production - The Idle Game: Product Catalog & Supply Chain Definitions
// Scalable data-driven structure supporting multi-tier inputs, sectors, and facilities // Clean modular orchestrator combining sector definitions
import { AGRICULTURE_SECTOR, AGRICULTURE_PRODUCTS } from './sectors/agriculture.js';
import { MATERIALS_SECTOR, MATERIALS_PRODUCTS } from './sectors/materials.js';
import { MINING_SECTOR, MINING_PRODUCTS } from './sectors/mining.js';
import { CHEMICALS_SECTOR, CHEMICALS_PRODUCTS } from './sectors/chemicals.js';
import { ELECTRONICS_SECTOR, ELECTRONICS_PRODUCTS } from './sectors/electronics.js';
import { HEAVY_INDUSTRY_SECTOR, HEAVY_INDUSTRY_PRODUCTS } from './sectors/heavyIndustry.js';
export const SECTORS = [ export const SECTORS = [
{ AGRICULTURE_SECTOR,
id: 'agriculture', MATERIALS_SECTOR,
name: 'Agriculture & Food', MINING_SECTOR,
icon: 'lucide:wheat', CHEMICALS_SECTOR,
color: '#16a34a', // Emerald Green ELECTRONICS_SECTOR,
description: 'Primary crops, livestock, milling, and commercial foodstuffs.' HEAVY_INDUSTRY_SECTOR
},
{
id: 'materials',
name: 'Forestry & Materials',
icon: 'lucide:trees',
color: '#ca8a04', // Amber/Wood
description: 'Timber harvesting, lumber mills, paper, and commercial furniture.'
},
{
id: 'mining',
name: 'Mining & Metallurgy',
icon: 'lucide:pickaxe',
color: '#475569', // Slate/Steel
description: 'Ore extraction, blast furnaces, alloy smelting, and heavy frames.'
},
{
id: 'chemicals',
name: 'Chemicals & Polymers',
icon: 'lucide:flask-conical',
color: '#0284c7', // Sky Blue
description: 'Crude oil refinement, plastics, polymers, and synthetic compounds.'
},
{
id: 'electronics',
name: 'High-Tech & Electronics',
icon: 'lucide:cpu',
color: '#7c3aed', // Purple
description: 'Purified silicon, microchips, circuit assemblies, and smart devices.'
},
{
id: 'heavy_industry',
name: 'Heavy Industry & Vehicles',
icon: 'lucide:truck',
color: '#ea580c', // Industrial Orange
description: 'Combustion engines, logistics fleets, industrial robotics, and transport.'
}
]; ];
export const PRODUCTS = [ export const PRODUCTS = [
// --- SECTOR 1: AGRICULTURE & FOOD --- ...AGRICULTURE_PRODUCTS,
{ ...MATERIALS_PRODUCTS,
id: 'wheat', ...MINING_PRODUCTS,
name: 'Grain (Wheat)', ...CHEMICALS_PRODUCTS,
sector: 'agriculture', ...ELECTRONICS_PRODUCTS,
tier: 1, ...HEAVY_INDUSTRY_PRODUCTS
icon: 'lucide:wheat',
color: '#65a30d',
unit: 'bushels',
inputs: [], // Raw extraction
outputAmount: 1,
baseCycleTime: 1.0,
unlockCost: 0,
unlockedDefault: true,
baseFacilityCost: 15,
baseManagerCost: 100,
baseLogisticsCost: 80,
basePrice: 1.50,
facilityName: 'Wheat Field',
managerName: 'Farm Overseer',
logisticsName: 'Grain Sifter',
description: 'Golden wheat harvested from fertile corporate acreage.'
},
{
id: 'flour',
name: 'Milled Flour',
sector: 'agriculture',
tier: 2,
icon: 'lucide:cookie',
color: '#84cc16',
unit: 'sacks',
inputs: [{ id: 'wheat', amount: 2 }],
outputAmount: 1,
baseCycleTime: 1.8,
unlockCost: 250,
unlockedDefault: false,
baseFacilityCost: 200,
baseManagerCost: 850,
baseLogisticsCost: 600,
basePrice: 6.20,
facilityName: 'Grain Mill',
managerName: 'Master Miller',
logisticsName: 'Flour Courier',
description: 'Fine milled wheat flour ready for commercial bakeries.'
},
{
id: 'sugar_beet',
name: 'Sugar Beet',
sector: 'agriculture',
tier: 1,
icon: 'lucide:sprout',
color: '#a3e635',
unit: 'crates',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.5,
unlockCost: 650,
unlockedDefault: false,
baseFacilityCost: 550,
baseManagerCost: 1800,
baseLogisticsCost: 1200,
basePrice: 4.80,
facilityName: 'Sugar Plantation',
managerName: 'Field Director',
logisticsName: 'Beet Hauler',
description: 'Sweet root vegetables cultivated for industrial sweetener extraction.'
},
{
id: 'refined_sugar',
name: 'Refined Sugar',
sector: 'agriculture',
tier: 2,
icon: 'lucide:candy',
color: '#e2e8f0',
unit: 'bags',
inputs: [{ id: 'sugar_beet', amount: 3 }],
outputAmount: 1,
baseCycleTime: 2.2,
unlockCost: 2200,
unlockedDefault: false,
baseFacilityCost: 1800,
baseManagerCost: 4500,
baseLogisticsCost: 3200,
basePrice: 22.00,
facilityName: 'Refinery Plant',
managerName: 'Sugar Chemist',
logisticsName: 'Bulk Distributor',
description: 'Pure granulated sucrose ready for food production.'
},
{
id: 'fresh_bread',
name: 'Artisan Bread',
sector: 'agriculture',
tier: 3,
icon: 'lucide:croissant',
color: '#f59e0b',
unit: 'loaves',
inputs: [{ id: 'flour', amount: 2 }, { id: 'refined_sugar', amount: 1 }],
outputAmount: 2,
baseCycleTime: 3.0,
unlockCost: 9500,
unlockedDefault: false,
baseFacilityCost: 7500,
baseManagerCost: 18000,
baseLogisticsCost: 12500,
basePrice: 58.00,
facilityName: 'Industrial Bakery',
managerName: 'Head Baker',
logisticsName: 'Delivery Fleet',
description: 'Freshly baked wholesale bread dispatched to grocery chains.'
},
{
id: 'packaged_meals',
name: 'Packaged Meal Kits',
sector: 'agriculture',
tier: 4,
icon: 'lucide:utensils',
color: '#10b981',
unit: 'cases',
inputs: [{ id: 'fresh_bread', amount: 3 }],
outputAmount: 1,
baseCycleTime: 4.5,
unlockCost: 45000,
unlockedDefault: false,
baseFacilityCost: 36000,
baseManagerCost: 75000,
baseLogisticsCost: 55000,
basePrice: 260.00,
facilityName: 'Food Processing Line',
managerName: 'Operations Lead',
logisticsName: 'Refrigerated Transport',
description: 'Ready-to-eat branded meal kits with high retail margin.'
},
// --- SECTOR 2: FORESTRY & MATERIALS ---
{
id: 'timber',
name: 'Raw Timber',
sector: 'materials',
tier: 1,
icon: 'lucide:trees',
color: '#b45309',
unit: 'logs',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.2,
unlockCost: 120,
unlockedDefault: false,
baseFacilityCost: 95,
baseManagerCost: 350,
baseLogisticsCost: 240,
basePrice: 2.80,
facilityName: 'Lumber Camp',
managerName: 'Forester Chief',
logisticsName: 'Log Carrier',
description: 'Mature softwood felled in managed timber estates.'
},
{
id: 'planks',
name: 'Milled Planks',
sector: 'materials',
tier: 2,
icon: 'lucide:boxes',
color: '#d97706',
unit: 'bundles',
inputs: [{ id: 'timber', amount: 2 }],
outputAmount: 1,
baseCycleTime: 2.0,
unlockCost: 1200,
unlockedDefault: false,
baseFacilityCost: 950,
baseManagerCost: 2800,
baseLogisticsCost: 2000,
basePrice: 11.50,
facilityName: 'Sawmill',
managerName: 'Sawyer Foreman',
logisticsName: 'Lumber Flatbed',
description: 'Standardized structural lumber for construction and joinery.'
},
{
id: 'paper',
name: 'Industrial Paper',
sector: 'materials',
tier: 2,
icon: 'lucide:file-text',
color: '#cbd5e1',
unit: 'rolls',
inputs: [{ id: 'timber', amount: 3 }],
outputAmount: 2,
baseCycleTime: 2.5,
unlockCost: 3500,
unlockedDefault: false,
baseFacilityCost: 2800,
baseManagerCost: 6500,
baseLogisticsCost: 4800,
basePrice: 16.00,
facilityName: 'Pulp Mill',
managerName: 'Chemical Pulpist',
logisticsName: 'Paper Dispatcher',
description: 'Heavy paper stock used for cardboard packaging and publishing.'
},
{
id: 'furniture',
name: 'Office Furniture',
sector: 'materials',
tier: 3,
icon: 'lucide:armchair',
color: '#92400e',
unit: 'sets',
inputs: [{ id: 'planks', amount: 3 }],
outputAmount: 1,
baseCycleTime: 3.8,
unlockCost: 18000,
unlockedDefault: false,
baseFacilityCost: 14000,
baseManagerCost: 35000,
baseLogisticsCost: 26000,
basePrice: 95.00,
facilityName: 'Joinery Workshop',
managerName: 'Master Carpenter',
logisticsName: 'Showroom Distributor',
description: 'Ergonomic executive desks, cabinetry, and boardroom chairs.'
},
// --- SECTOR 3: MINING & METALLURGY ---
{
id: 'iron_ore',
name: 'Iron Ore',
sector: 'mining',
tier: 1,
icon: 'lucide:mountain',
color: '#64748b',
unit: 'tons',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.6,
unlockCost: 1500,
unlockedDefault: false,
baseFacilityCost: 1200,
baseManagerCost: 3500,
baseLogisticsCost: 2400,
basePrice: 12.00,
facilityName: 'Open-Pit Iron Mine',
managerName: 'Mining Superintendant',
logisticsName: 'Ore Hopper Train',
description: 'Dense hematite extracted from open-pit mineral deposits.'
},
{
id: 'coal',
name: 'Coking Coal',
sector: 'mining',
tier: 1,
icon: 'lucide:flame',
color: '#334155',
unit: 'tons',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.8,
unlockCost: 2800,
unlockedDefault: false,
baseFacilityCost: 2200,
baseManagerCost: 5500,
baseLogisticsCost: 3800,
basePrice: 15.00,
facilityName: 'Coal Pit',
managerName: 'Pit Supervisor',
logisticsName: 'Coal Conveyor',
description: 'High-energy carbon fuel essential for blast furnace reduction.'
},
{
id: 'steel_ingots',
name: 'Structural Steel',
sector: 'mining',
tier: 2,
icon: 'lucide:anvil',
color: '#94a3b8',
unit: 'billets',
inputs: [{ id: 'iron_ore', amount: 2 }, { id: 'coal', amount: 1 }],
outputAmount: 1,
baseCycleTime: 3.2,
unlockCost: 12500,
unlockedDefault: false,
baseFacilityCost: 9800,
baseManagerCost: 24000,
baseLogisticsCost: 17000,
basePrice: 72.00,
facilityName: 'Blast Smelter',
managerName: 'Foundry Master',
logisticsName: 'Heavy Rail Freight',
description: 'High-tensile carbon steel alloys for industrial manufacturing.'
},
{
id: 'metal_frames',
name: 'Heavy Frameworks',
sector: 'mining',
tier: 3,
icon: 'lucide:construction',
color: '#475569',
unit: 'girders',
inputs: [{ id: 'steel_ingots', amount: 3 }],
outputAmount: 1,
baseCycleTime: 4.6,
unlockCost: 65000,
unlockedDefault: false,
baseFacilityCost: 52000,
baseManagerCost: 110000,
baseLogisticsCost: 80000,
basePrice: 320.00,
facilityName: 'Fabrication Plant',
managerName: 'Chief Fabricator',
logisticsName: 'Heavy Hauler',
description: 'Rigid reinforced metal chassis and skyscraper beams.'
},
// --- SECTOR 4: CHEMICALS & POLYMERS ---
{
id: 'crude_oil',
name: 'Crude Oil',
sector: 'chemicals',
tier: 1,
icon: 'lucide:droplet',
color: '#1e293b',
unit: 'barrels',
inputs: [],
outputAmount: 1,
baseCycleTime: 2.0,
unlockCost: 25000,
unlockedDefault: false,
baseFacilityCost: 20000,
baseManagerCost: 48000,
baseLogisticsCost: 35000,
basePrice: 55.00,
facilityName: 'Drilling Derrick',
managerName: 'Derrick Engineer',
logisticsName: 'Pipeline Station',
description: 'Black liquid gold pumped from deep continental wells.'
},
{
id: 'silica_sand',
name: 'Silica Quartz Sand',
sector: 'chemicals',
tier: 1,
icon: 'lucide:sparkles',
color: '#38bdf8',
unit: 'drums',
inputs: [],
outputAmount: 1,
baseCycleTime: 2.2,
unlockCost: 45000,
unlockedDefault: false,
baseFacilityCost: 36000,
baseManagerCost: 75000,
baseLogisticsCost: 52000,
basePrice: 85.00,
facilityName: 'Quartz Quarry',
managerName: 'Geological Analyst',
logisticsName: 'Quartz Transport',
description: 'High-purity quartz crystals mined for semiconductor refining.'
},
{
id: 'polymers',
name: 'Industrial Polymers',
sector: 'chemicals',
tier: 2,
icon: 'lucide:test-tube',
color: '#0ea5e9',
unit: 'pellets',
inputs: [{ id: 'crude_oil', amount: 2 }],
outputAmount: 1,
baseCycleTime: 3.5,
unlockCost: 110000,
unlockedDefault: false,
baseFacilityCost: 88000,
baseManagerCost: 175000,
baseLogisticsCost: 120000,
basePrice: 210.00,
facilityName: 'Cracking Facility',
managerName: 'Petrochemical Lead',
logisticsName: 'Chemical Tanker',
description: 'Versatile synthetic thermoplastic pellets for manufacturing.'
},
{
id: 'silicon',
name: 'Purified Silicon',
sector: 'chemicals',
tier: 2,
icon: 'lucide:gem',
color: '#06b6d4',
unit: 'ingots',
inputs: [{ id: 'silica_sand', amount: 2 }],
outputAmount: 1,
baseCycleTime: 4.0,
unlockCost: 220000,
unlockedDefault: false,
baseFacilityCost: 175000,
baseManagerCost: 350000,
baseLogisticsCost: 240000,
basePrice: 420.00,
facilityName: 'Silicon Smelter',
managerName: 'Materials Scientist',
logisticsName: 'Clean Ingot Carrier',
description: '99.999% pure electronic grade monocrystalline silicon.'
},
// --- SECTOR 5: HIGH-TECH & ELECTRONICS ---
{
id: 'microchips',
name: 'Silicon Microchips',
sector: 'electronics',
tier: 3,
icon: 'lucide:cpu',
color: '#8b5cf6',
unit: 'chips',
inputs: [{ id: 'silicon', amount: 1 }, { id: 'polymers', amount: 1 }],
outputAmount: 2,
baseCycleTime: 4.8,
unlockCost: 550000,
unlockedDefault: false,
baseFacilityCost: 440000,
baseManagerCost: 850000,
baseLogisticsCost: 600000,
basePrice: 650.00,
facilityName: 'Cleanroom Fab',
managerName: 'Lithography Director',
logisticsName: 'Precision Courier',
description: 'Nanometer-scale integrated circuits etched via photolithography.'
},
{
id: 'circuit_boards',
name: 'PCB Motherboards',
sector: 'electronics',
tier: 3,
icon: 'lucide:circuit-board',
color: '#a855f7',
unit: 'boards',
inputs: [{ id: 'microchips', amount: 2 }, { id: 'polymers', amount: 2 }],
outputAmount: 1,
baseCycleTime: 5.5,
unlockCost: 1500000,
unlockedDefault: false,
baseFacilityCost: 1200000,
baseManagerCost: 2400000,
baseLogisticsCost: 1600000,
basePrice: 2200.00,
facilityName: 'PCB Assembly Line',
managerName: 'SMT Chief',
logisticsName: 'Static-Safe Transport',
description: 'Multi-layer printed circuit boards populated with components.'
},
{
id: 'smartphones',
name: 'Consumer Smartphones',
sector: 'electronics',
tier: 4,
icon: 'lucide:smartphone',
color: '#ec4899',
unit: 'devices',
inputs: [{ id: 'circuit_boards', amount: 1 }, { id: 'polymers', amount: 2 }],
outputAmount: 1,
baseCycleTime: 6.5,
unlockCost: 5000000,
unlockedDefault: false,
baseFacilityCost: 4000000,
baseManagerCost: 8000000,
baseLogisticsCost: 5500000,
basePrice: 6800.00,
facilityName: 'Tech Assembly Plant',
managerName: 'VP of Electronics',
logisticsName: 'Global Freight Air',
description: 'Flagship handheld smartphones shipped to worldwide retail.'
},
// --- SECTOR 6: HEAVY INDUSTRY & VEHICLES ---
{
id: 'engines',
name: 'Industrial Engines',
sector: 'heavy_industry',
tier: 3,
icon: 'lucide:cog',
color: '#f97316',
unit: 'motors',
inputs: [{ id: 'steel_ingots', amount: 3 }, { id: 'metal_frames', amount: 1 }],
outputAmount: 1,
baseCycleTime: 6.0,
unlockCost: 3500000,
unlockedDefault: false,
baseFacilityCost: 2800000,
baseManagerCost: 5500000,
baseLogisticsCost: 3800000,
basePrice: 4200.00,
facilityName: 'Motor Foundry',
managerName: 'Engine Master',
logisticsName: 'Crated Transport',
description: 'High-torque turbodiesel and hybrid electric powerplants.'
},
{
id: 'commercial_trucks',
name: 'Commercial Trucks',
sector: 'heavy_industry',
tier: 4,
icon: 'lucide:truck',
color: '#dc2626',
unit: 'trucks',
inputs: [{ id: 'engines', amount: 1 }, { id: 'metal_frames', amount: 2 }, { id: 'circuit_boards', amount: 1 }],
outputAmount: 1,
baseCycleTime: 8.5,
unlockCost: 15000000,
unlockedDefault: false,
baseFacilityCost: 12000000,
baseManagerCost: 22000000,
baseLogisticsCost: 15000000,
basePrice: 26500.00,
facilityName: 'Vehicle Mega-Factory',
managerName: 'Plant General Manager',
logisticsName: 'Carrier Fleet',
description: 'Heavy duty articulated transport trucks for national logistics.'
}
]; ];
// Helper to look up a product by ID // Helper to look up a product by ID

View file

@ -0,0 +1,143 @@
// Sector 1: Agriculture & Food
export const AGRICULTURE_SECTOR = {
id: 'agriculture',
name: 'Agriculture & Food',
icon: 'lucide:wheat',
color: '#16a34a',
description: 'Primary crops, livestock, milling, and commercial foodstuffs.'
};
export const AGRICULTURE_PRODUCTS = [
{
id: 'wheat',
name: 'Grain (Wheat)',
sector: 'agriculture',
tier: 1,
icon: 'lucide:wheat',
color: '#65a30d',
unit: 'bushels',
inputs: [], // Raw extraction
outputAmount: 1,
baseCycleTime: 1.0,
unlockCost: 0,
unlockedDefault: true,
baseFacilityCost: 15,
baseManagerCost: 100,
baseLogisticsCost: 80,
basePrice: 1.50,
facilityName: 'Wheat Field',
managerName: 'Farm Overseer',
logisticsName: 'Grain Sifter',
description: 'Golden wheat harvested from fertile corporate acreage.'
},
{
id: 'flour',
name: 'Milled Flour',
sector: 'agriculture',
tier: 2,
icon: 'lucide:cookie',
color: '#84cc16',
unit: 'sacks',
inputs: [{ id: 'wheat', amount: 2 }],
outputAmount: 1,
baseCycleTime: 1.8,
unlockCost: 250,
unlockedDefault: false,
baseFacilityCost: 200,
baseManagerCost: 850,
baseLogisticsCost: 600,
basePrice: 6.20,
facilityName: 'Grain Mill',
managerName: 'Master Miller',
logisticsName: 'Flour Courier',
description: 'Fine milled wheat flour ready for commercial bakeries.'
},
{
id: 'sugar_beet',
name: 'Sugar Beet',
sector: 'agriculture',
tier: 1,
icon: 'lucide:sprout',
color: '#a3e635',
unit: 'crates',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.5,
unlockCost: 650,
unlockedDefault: false,
baseFacilityCost: 550,
baseManagerCost: 1800,
baseLogisticsCost: 1200,
basePrice: 4.80,
facilityName: 'Sugar Plantation',
managerName: 'Field Director',
logisticsName: 'Beet Hauler',
description: 'Sweet root vegetables cultivated for industrial sweetener extraction.'
},
{
id: 'refined_sugar',
name: 'Refined Sugar',
sector: 'agriculture',
tier: 2,
icon: 'lucide:candy',
color: '#e2e8f0',
unit: 'bags',
inputs: [{ id: 'sugar_beet', amount: 3 }],
outputAmount: 1,
baseCycleTime: 2.2,
unlockCost: 2200,
unlockedDefault: false,
baseFacilityCost: 1800,
baseManagerCost: 4500,
baseLogisticsCost: 3200,
basePrice: 22.00,
facilityName: 'Refinery Plant',
managerName: 'Sugar Chemist',
logisticsName: 'Bulk Distributor',
description: 'Pure granulated sucrose ready for food production.'
},
{
id: 'fresh_bread',
name: 'Artisan Bread',
sector: 'agriculture',
tier: 3,
icon: 'lucide:croissant',
color: '#f59e0b',
unit: 'loaves',
inputs: [{ id: 'flour', amount: 2 }, { id: 'refined_sugar', amount: 1 }],
outputAmount: 2,
baseCycleTime: 3.0,
unlockCost: 9500,
unlockedDefault: false,
baseFacilityCost: 7500,
baseManagerCost: 18000,
baseLogisticsCost: 12500,
basePrice: 58.00,
facilityName: 'Industrial Bakery',
managerName: 'Head Baker',
logisticsName: 'Delivery Fleet',
description: 'Freshly baked wholesale bread dispatched to grocery chains.'
},
{
id: 'packaged_meals',
name: 'Packaged Meal Kits',
sector: 'agriculture',
tier: 4,
icon: 'lucide:utensils',
color: '#10b981',
unit: 'cases',
inputs: [{ id: 'fresh_bread', amount: 3 }],
outputAmount: 1,
baseCycleTime: 4.5,
unlockCost: 45000,
unlockedDefault: false,
baseFacilityCost: 36000,
baseManagerCost: 75000,
baseLogisticsCost: 55000,
basePrice: 260.00,
facilityName: 'Food Processing Line',
managerName: 'Operations Lead',
logisticsName: 'Refrigerated Transport',
description: 'Ready-to-eat branded meal kits with high retail margin.'
}
];

View file

@ -0,0 +1,99 @@
// Sector 4: Chemicals & Polymers
export const CHEMICALS_SECTOR = {
id: 'chemicals',
name: 'Chemicals & Polymers',
icon: 'lucide:flask-conical',
color: '#0284c7',
description: 'Crude oil refinement, plastics, polymers, and synthetic compounds.'
};
export const CHEMICALS_PRODUCTS = [
{
id: 'crude_oil',
name: 'Crude Oil',
sector: 'chemicals',
tier: 1,
icon: 'lucide:droplet',
color: '#1e293b',
unit: 'barrels',
inputs: [],
outputAmount: 1,
baseCycleTime: 2.0,
unlockCost: 25000,
unlockedDefault: false,
baseFacilityCost: 20000,
baseManagerCost: 48000,
baseLogisticsCost: 35000,
basePrice: 55.00,
facilityName: 'Drilling Derrick',
managerName: 'Derrick Engineer',
logisticsName: 'Pipeline Station',
description: 'Black liquid gold pumped from deep continental wells.'
},
{
id: 'silica_sand',
name: 'Silica Quartz Sand',
sector: 'chemicals',
tier: 1,
icon: 'lucide:sparkles',
color: '#38bdf8',
unit: 'drums',
inputs: [],
outputAmount: 1,
baseCycleTime: 2.2,
unlockCost: 45000,
unlockedDefault: false,
baseFacilityCost: 36000,
baseManagerCost: 75000,
baseLogisticsCost: 52000,
basePrice: 85.00,
facilityName: 'Quartz Quarry',
managerName: 'Geological Analyst',
logisticsName: 'Quartz Transport',
description: 'High-purity quartz crystals mined for semiconductor refining.'
},
{
id: 'polymers',
name: 'Industrial Polymers',
sector: 'chemicals',
tier: 2,
icon: 'lucide:test-tube',
color: '#0ea5e9',
unit: 'pellets',
inputs: [{ id: 'crude_oil', amount: 2 }],
outputAmount: 1,
baseCycleTime: 3.5,
unlockCost: 110000,
unlockedDefault: false,
baseFacilityCost: 88000,
baseManagerCost: 175000,
baseLogisticsCost: 120000,
basePrice: 210.00,
facilityName: 'Cracking Facility',
managerName: 'Petrochemical Lead',
logisticsName: 'Chemical Tanker',
description: 'Versatile synthetic thermoplastic pellets for manufacturing.'
},
{
id: 'silicon',
name: 'Purified Silicon',
sector: 'chemicals',
tier: 2,
icon: 'lucide:gem',
color: '#06b6d4',
unit: 'ingots',
inputs: [{ id: 'silica_sand', amount: 2 }],
outputAmount: 1,
baseCycleTime: 4.0,
unlockCost: 220000,
unlockedDefault: false,
baseFacilityCost: 175000,
baseManagerCost: 350000,
baseLogisticsCost: 240000,
basePrice: 420.00,
facilityName: 'Silicon Smelter',
managerName: 'Materials Scientist',
logisticsName: 'Clean Ingot Carrier',
description: '99.999% pure electronic grade monocrystalline silicon.'
}
];

View file

@ -0,0 +1,77 @@
// Sector 5: High-Tech & Electronics
export const ELECTRONICS_SECTOR = {
id: 'electronics',
name: 'High-Tech & Electronics',
icon: 'lucide:cpu',
color: '#7c3aed',
description: 'Purified silicon, microchips, circuit assemblies, and smart devices.'
};
export const ELECTRONICS_PRODUCTS = [
{
id: 'microchips',
name: 'Silicon Microchips',
sector: 'electronics',
tier: 3,
icon: 'lucide:cpu',
color: '#8b5cf6',
unit: 'chips',
inputs: [{ id: 'silicon', amount: 1 }, { id: 'polymers', amount: 1 }],
outputAmount: 2,
baseCycleTime: 4.8,
unlockCost: 550000,
unlockedDefault: false,
baseFacilityCost: 440000,
baseManagerCost: 850000,
baseLogisticsCost: 600000,
basePrice: 650.00,
facilityName: 'Cleanroom Fab',
managerName: 'Lithography Director',
logisticsName: 'Precision Courier',
description: 'Nanometer-scale integrated circuits etched via photolithography.'
},
{
id: 'circuit_boards',
name: 'PCB Motherboards',
sector: 'electronics',
tier: 3,
icon: 'lucide:circuit-board',
color: '#a855f7',
unit: 'boards',
inputs: [{ id: 'microchips', amount: 2 }, { id: 'polymers', amount: 2 }],
outputAmount: 1,
baseCycleTime: 5.5,
unlockCost: 1500000,
unlockedDefault: false,
baseFacilityCost: 1200000,
baseManagerCost: 2400000,
baseLogisticsCost: 1600000,
basePrice: 2200.00,
facilityName: 'PCB Assembly Line',
managerName: 'SMT Chief',
logisticsName: 'Static-Safe Transport',
description: 'Multi-layer printed circuit boards populated with components.'
},
{
id: 'smartphones',
name: 'Consumer Smartphones',
sector: 'electronics',
tier: 4,
icon: 'lucide:smartphone',
color: '#ec4899',
unit: 'devices',
inputs: [{ id: 'circuit_boards', amount: 1 }, { id: 'polymers', amount: 2 }],
outputAmount: 1,
baseCycleTime: 6.5,
unlockCost: 5000000,
unlockedDefault: false,
baseFacilityCost: 4000000,
baseManagerCost: 8000000,
baseLogisticsCost: 5500000,
basePrice: 6800.00,
facilityName: 'Tech Assembly Plant',
managerName: 'VP of Electronics',
logisticsName: 'Global Freight Air',
description: 'Flagship handheld smartphones shipped to worldwide retail.'
}
];

View file

@ -0,0 +1,55 @@
// Sector 6: Heavy Industry & Vehicles
export const HEAVY_INDUSTRY_SECTOR = {
id: 'heavy_industry',
name: 'Heavy Industry & Vehicles',
icon: 'lucide:truck',
color: '#ea580c',
description: 'Combustion engines, logistics fleets, industrial robotics, and transport.'
};
export const HEAVY_INDUSTRY_PRODUCTS = [
{
id: 'engines',
name: 'Industrial Engines',
sector: 'heavy_industry',
tier: 3,
icon: 'lucide:cog',
color: '#f97316',
unit: 'motors',
inputs: [{ id: 'steel_ingots', amount: 3 }, { id: 'metal_frames', amount: 1 }],
outputAmount: 1,
baseCycleTime: 6.0,
unlockCost: 3500000,
unlockedDefault: false,
baseFacilityCost: 2800000,
baseManagerCost: 5500000,
baseLogisticsCost: 3800000,
basePrice: 4200.00,
facilityName: 'Motor Foundry',
managerName: 'Engine Master',
logisticsName: 'Crated Transport',
description: 'High-torque turbodiesel and hybrid electric powerplants.'
},
{
id: 'commercial_trucks',
name: 'Commercial Trucks',
sector: 'heavy_industry',
tier: 4,
icon: 'lucide:truck',
color: '#dc2626',
unit: 'trucks',
inputs: [{ id: 'engines', amount: 1 }, { id: 'metal_frames', amount: 2 }, { id: 'circuit_boards', amount: 1 }],
outputAmount: 1,
baseCycleTime: 8.5,
unlockCost: 15000000,
unlockedDefault: false,
baseFacilityCost: 12000000,
baseManagerCost: 22000000,
baseLogisticsCost: 15000000,
basePrice: 26500.00,
facilityName: 'Vehicle Mega-Factory',
managerName: 'Plant General Manager',
logisticsName: 'Carrier Fleet',
description: 'Heavy duty articulated transport trucks for national logistics.'
}
];

View file

@ -0,0 +1,99 @@
// Sector 2: Forestry & Materials
export const MATERIALS_SECTOR = {
id: 'materials',
name: 'Forestry & Materials',
icon: 'lucide:trees',
color: '#ca8a04',
description: 'Timber harvesting, lumber mills, paper, and commercial furniture.'
};
export const MATERIALS_PRODUCTS = [
{
id: 'timber',
name: 'Raw Timber',
sector: 'materials',
tier: 1,
icon: 'lucide:trees',
color: '#b45309',
unit: 'logs',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.2,
unlockCost: 120,
unlockedDefault: false,
baseFacilityCost: 95,
baseManagerCost: 350,
baseLogisticsCost: 240,
basePrice: 2.80,
facilityName: 'Lumber Camp',
managerName: 'Forester Chief',
logisticsName: 'Log Carrier',
description: 'Mature softwood felled in managed timber estates.'
},
{
id: 'planks',
name: 'Milled Planks',
sector: 'materials',
tier: 2,
icon: 'lucide:boxes',
color: '#d97706',
unit: 'bundles',
inputs: [{ id: 'timber', amount: 2 }],
outputAmount: 1,
baseCycleTime: 2.0,
unlockCost: 1200,
unlockedDefault: false,
baseFacilityCost: 950,
baseManagerCost: 2800,
baseLogisticsCost: 2000,
basePrice: 11.50,
facilityName: 'Sawmill',
managerName: 'Sawyer Foreman',
logisticsName: 'Lumber Flatbed',
description: 'Standardized structural lumber for construction and joinery.'
},
{
id: 'paper',
name: 'Industrial Paper',
sector: 'materials',
tier: 2,
icon: 'lucide:file-text',
color: '#cbd5e1',
unit: 'rolls',
inputs: [{ id: 'timber', amount: 3 }],
outputAmount: 2,
baseCycleTime: 2.5,
unlockCost: 3500,
unlockedDefault: false,
baseFacilityCost: 2800,
baseManagerCost: 6500,
baseLogisticsCost: 4800,
basePrice: 16.00,
facilityName: 'Pulp Mill',
managerName: 'Chemical Pulpist',
logisticsName: 'Paper Dispatcher',
description: 'Heavy paper stock used for cardboard packaging and publishing.'
},
{
id: 'furniture',
name: 'Office Furniture',
sector: 'materials',
tier: 3,
icon: 'lucide:armchair',
color: '#92400e',
unit: 'sets',
inputs: [{ id: 'planks', amount: 3 }],
outputAmount: 1,
baseCycleTime: 3.8,
unlockCost: 18000,
unlockedDefault: false,
baseFacilityCost: 14000,
baseManagerCost: 35000,
baseLogisticsCost: 26000,
basePrice: 95.00,
facilityName: 'Joinery Workshop',
managerName: 'Master Carpenter',
logisticsName: 'Showroom Distributor',
description: 'Ergonomic executive desks, cabinetry, and boardroom chairs.'
}
];

View file

@ -0,0 +1,99 @@
// Sector 3: Mining & Metallurgy
export const MINING_SECTOR = {
id: 'mining',
name: 'Mining & Metallurgy',
icon: 'lucide:pickaxe',
color: '#475569',
description: 'Ore extraction, blast furnaces, alloy smelting, and heavy frames.'
};
export const MINING_PRODUCTS = [
{
id: 'iron_ore',
name: 'Iron Ore',
sector: 'mining',
tier: 1,
icon: 'lucide:mountain',
color: '#64748b',
unit: 'tons',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.6,
unlockCost: 1500,
unlockedDefault: false,
baseFacilityCost: 1200,
baseManagerCost: 3500,
baseLogisticsCost: 2400,
basePrice: 12.00,
facilityName: 'Open-Pit Iron Mine',
managerName: 'Mining Superintendant',
logisticsName: 'Ore Hopper Train',
description: 'Dense hematite extracted from open-pit mineral deposits.'
},
{
id: 'coal',
name: 'Coking Coal',
sector: 'mining',
tier: 1,
icon: 'lucide:flame',
color: '#334155',
unit: 'tons',
inputs: [],
outputAmount: 1,
baseCycleTime: 1.8,
unlockCost: 2800,
unlockedDefault: false,
baseFacilityCost: 2200,
baseManagerCost: 5500,
baseLogisticsCost: 3800,
basePrice: 15.00,
facilityName: 'Coal Pit',
managerName: 'Pit Supervisor',
logisticsName: 'Coal Conveyor',
description: 'High-energy carbon fuel essential for blast furnace reduction.'
},
{
id: 'steel_ingots',
name: 'Structural Steel',
sector: 'mining',
tier: 2,
icon: 'lucide:anvil',
color: '#94a3b8',
unit: 'billets',
inputs: [{ id: 'iron_ore', amount: 2 }, { id: 'coal', amount: 1 }],
outputAmount: 1,
baseCycleTime: 3.2,
unlockCost: 12500,
unlockedDefault: false,
baseFacilityCost: 9800,
baseManagerCost: 24000,
baseLogisticsCost: 17000,
basePrice: 72.00,
facilityName: 'Blast Smelter',
managerName: 'Foundry Master',
logisticsName: 'Heavy Rail Freight',
description: 'High-tensile carbon steel alloys for industrial manufacturing.'
},
{
id: 'metal_frames',
name: 'Heavy Frameworks',
sector: 'mining',
tier: 3,
icon: 'lucide:construction',
color: '#475569',
unit: 'girders',
inputs: [{ id: 'steel_ingots', amount: 3 }],
outputAmount: 1,
baseCycleTime: 4.6,
unlockCost: 65000,
unlockedDefault: false,
baseFacilityCost: 52000,
baseManagerCost: 110000,
baseLogisticsCost: 80000,
basePrice: 320.00,
facilityName: 'Fabrication Plant',
managerName: 'Chief Fabricator',
logisticsName: 'Heavy Hauler',
description: 'Rigid reinforced metal chassis and skyscraper beams.'
}
];

View file

@ -0,0 +1,310 @@
// Production UI: Individual Facility Card (HTML Mount and Fine-grained DOM update)
import { getProduct, getNextMilestone, getUnitMilestoneMultiplier, SECTORS } from '../../data/products.js';
import { fmtCurrency, fmtNum } from '../../core/formatters.js';
import { calculateMultiCost, getProductOutputRates } from './ratesCalculator.js';
export function renderFacilityCardHtml(p, pState, state, buyMode) {
const sector = SECTORS.find(s => s.id === p.sector);
const isSelected = state.selectedProductId === p.id;
if (!pState.unlocked) {
const canAffordUnlock = state.cash >= p.unlockCost;
return `
<div class="facility-card locked-card ${isSelected ? 'selected' : ''}" id="card-${p.id}" 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'} &bull; 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" id="btn-unlock-${p.id}" data-product="${p.id}" ${canAffordUnlock ? '' : 'disabled'}>
<iconify-icon icon="lucide:lock-open"></iconify-icon>
<span>Acquire Permit (${fmtCurrency(p.unlockCost)})</span>
</button>
</div>
</div>
`;
}
const nextM = getNextMilestone(pState.lines);
const rates = getProductOutputRates(p, pState, state);
const milestoneMult = rates.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;
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' : ''}" id="chip-${p.id}-${inp.id}" title="${inpProduct?.name}">
<iconify-icon icon="${inpProduct?.icon || 'lucide:box'}"></iconify-icon>
${inpProduct?.name}: <strong id="chip-val-${p.id}-${inp.id}">${fmtNum(currentStock, 0)}</strong> / ${fmtNum(neededPerLine, 0)}
</span>
`;
}).join('')}
</div>
</div>
`;
}
let statusBadge = '';
if (pState.isBottlenecked) {
statusBadge = `<span class="status-badge badge-warning" id="badge-${p.id}"><iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs</span>`;
} else if (pState.lines > 0) {
statusBadge = `<span class="status-badge badge-success" id="badge-${p.id}"><iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)</span>`;
} else {
statusBadge = `<span class="status-badge badge-idle" id="badge-${p.id}">Idle (0 Lines)</span>`;
}
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
return `
<div class="facility-card active-card ${isSelected ? 'selected' : ''}" id="card-${p.id}" 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} &bull; Tier ${p.tier}
</span>
<div id="badge-wrap-${p.id}">${statusBadge}</div>
</div>
<h3 class="facility-name">${p.name}</h3>
<span class="facility-type-sub" id="type-sub-${p.id}">${p.facilityName} &bull; <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" id="progress-${p.id}" style="width: ${progressPct}%; background-color: ${p.color};"></div>
</div>
<!-- Stock, Velocity and Revenue Summary -->
<div class="facility-stats-grid">
<div class="stat-cell">
<span class="stat-label">Stockpile:</span>
<span class="stat-value font-mono"><strong id="stock-${p.id}">${fmtNum(pState.stock, 0)}</strong> ${p.unit}</span>
</div>
<div class="stat-cell">
<span class="stat-label">Production Output:</span>
<span class="stat-value font-mono ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}" id="rate-units-${p.id}">
${rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`}
</span>
</div>
<div class="stat-cell">
<span class="stat-label">Logistics Rev:</span>
<span class="stat-value font-mono text-profit" id="rate-rev-${p.id}">+$${fmtNum(rates.revRate, 2)}/s</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" title="Logistics fleets automatically sell stockpiled goods continuously onto the wholesale market (0.75 units/sec per fleet)">
<span class="stat-label">Logistics Auto-Sell:</span>
<span class="stat-value font-mono" id="logistics-${p.id}">${pState.logistics || 0} fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)}/s)</span>
</div>
<div class="stat-cell">
<span class="stat-label">Milestone Eff:</span>
<span class="stat-value font-mono" id="eff-${p.id}">${milestoneMult}x Speed</span>
</div>
</div>
${inputsHtml}
<!-- Facility Actions Bar -->
<div class="facility-actions-row">
<button class="btn-corp-action btn-cycle" data-product="${p.id}" title="Run 1 manual production cycle">
<iconify-icon icon="lucide:play"></iconify-icon>
<span>Run Batch</span>
</button>
<button class="btn-primary-corp btn-expand" id="btn-expand-${p.id}" data-product="${p.id}" ${canAffordBuy ? '' : 'disabled'} title="Add more production lines to increase output volume">
<iconify-icon icon="lucide:plus-circle"></iconify-icon>
<span id="btn-expand-text-${p.id}">+${calc.count} Lines (${fmtCurrency(calc.cost)})</span>
</button>
${!pState.manager ? `
<button class="btn-corp-secondary btn-hire-mgr" id="btn-hire-${p.id}" data-product="${p.id}" ${canAffordManager ? '' : 'disabled'} title="Hires a Facility Lead who automatically runs production continuously without clicking">
<iconify-icon icon="lucide:user-check"></iconify-icon>
<span>Hire Lead (${fmtCurrency(managerCost)})</span>
</button>
` : `
<span class="mgr-hired-badge" title="Facility Lead hired: 100% automated continuous production active">
<iconify-icon icon="lucide:bot"></iconify-icon> <span>Lead Active (Auto)</span>
</span>
`}
<button class="btn-corp-secondary btn-expand-logistics" id="btn-logistics-${p.id}" data-product="${p.id}" ${canAffordLogistics ? '' : 'disabled'} title="Deploys an automated distribution fleet (+0.75 units/sec passive wholesale sales)">
<iconify-icon icon="lucide:truck"></iconify-icon>
<span id="btn-logistics-text-${p.id}">+Fleet (${fmtCurrency(logisticsCost)})</span>
</button>
</div>
</div>
`;
}
export function updateFacilityCardDom(p, pState, state, buyMode) {
const cash = state.cash;
if (!pState.unlocked) {
const btnUnlock = document.getElementById(`btn-unlock-${p.id}`);
if (btnUnlock) {
const canAfford = cash >= p.unlockCost;
if (btnUnlock.disabled !== !canAfford) {
btnUnlock.disabled = !canAfford;
}
}
return;
}
// Live Individual Factory Rates & Output
const rates = getProductOutputRates(p, pState, state);
const elRateUnits = document.getElementById(`rate-units-${p.id}`);
if (elRateUnits) {
const unitsText = rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`;
if (elRateUnits.textContent !== unitsText) {
elRateUnits.textContent = unitsText;
}
elRateUnits.className = `stat-value font-mono ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}`;
}
const elRateRev = document.getElementById(`rate-rev-${p.id}`);
if (elRateRev) {
const revText = `+$${fmtNum(rates.revRate, 2)}/s`;
if (elRateRev.textContent !== revText) {
elRateRev.textContent = revText;
}
}
const elEff = document.getElementById(`eff-${p.id}`);
if (elEff) {
const effText = `${rates.milestoneMult}x Speed`;
if (elEff.textContent !== effText) {
elEff.textContent = effText;
}
}
const elLogistics = document.getElementById(`logistics-${p.id}`);
if (elLogistics) {
const count = pState.logistics || 0;
const logText = `${count} fleets (${fmtNum(count * 0.75, 2)}/s)`;
if (elLogistics.textContent !== logText) {
elLogistics.textContent = logText;
}
}
const elProgress = document.getElementById(`progress-${p.id}`);
if (elProgress) {
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
elProgress.style.width = `${progressPct}%`;
}
// Stockpile Text
const elStock = document.getElementById(`stock-${p.id}`);
if (elStock) {
const stockText = fmtNum(pState.stock, 0);
if (elStock.textContent !== stockText) {
elStock.textContent = stockText;
}
}
// Status Badge
const elBadge = document.getElementById(`badge-${p.id}`);
if (elBadge) {
const isBottlenecked = pState.isBottlenecked;
const hasWarning = elBadge.classList.contains('badge-warning');
if (isBottlenecked && !hasWarning) {
elBadge.className = 'status-badge badge-warning';
elBadge.innerHTML = `<iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs`;
} else if (!isBottlenecked && hasWarning) {
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
elBadge.className = 'status-badge badge-success';
elBadge.innerHTML = `<iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)`;
}
}
// Input Chips Stock
if (p.inputs && p.inputs.length > 0) {
for (const inp of p.inputs) {
const currentStock = state.products[inp.id]?.stock || 0;
const elChipVal = document.getElementById(`chip-val-${p.id}-${inp.id}`);
if (elChipVal) {
const textVal = fmtNum(currentStock, 0);
if (elChipVal.textContent !== textVal) {
elChipVal.textContent = textVal;
}
}
const elChip = document.getElementById(`chip-${p.id}-${inp.id}`);
if (elChip) {
const needed = inp.amount * pState.lines;
const isLow = currentStock < needed;
elChip.classList.toggle('low-stock', isLow);
}
}
}
// Expand Button disabled state
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
const elBtnExpand = document.getElementById(`btn-expand-${p.id}`);
if (elBtnExpand) {
const canAfford = cash >= calc.cost;
if (elBtnExpand.disabled !== !canAfford) {
elBtnExpand.disabled = !canAfford;
}
}
// Hire Manager disabled state
const elBtnHire = document.getElementById(`btn-hire-${p.id}`);
if (elBtnHire) {
const canAfford = cash >= p.baseManagerCost;
if (elBtnHire.disabled !== !canAfford) {
elBtnHire.disabled = !canAfford;
}
}
// 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) {
const canAfford = cash >= logisticsCost;
if (elBtnLogistics.disabled !== !canAfford) {
elBtnLogistics.disabled = !canAfford;
}
const elBtnLogisticsText = document.getElementById(`btn-logistics-text-${p.id}`);
if (elBtnLogisticsText) {
const text = `+Fleet (${fmtCurrency(logisticsCost)})`;
if (elBtnLogisticsText.textContent !== text) {
elBtnLogisticsText.textContent = text;
}
}
}
// Selected Card class
const elCard = document.getElementById(`card-${p.id}`);
if (elCard) {
const isSelected = state.selectedProductId === p.id;
elCard.classList.toggle('selected', isSelected);
}
}

View file

@ -0,0 +1,79 @@
// Production UI: Cost & Rate Calculations
import { getNextMilestone, getUnitMilestoneMultiplier } from '../../data/products.js';
export function calculateMultiCost(baseCost, currentCount, multiplier) {
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;
}
let totalCost = 0;
for (let i = 0; i < countToBuy; i++) {
totalCost += baseCost * Math.pow(growth, currentCount + i);
}
return { count: countToBuy, cost: totalCost };
}
/**
* Computes live individual factory output rates (units/s and wholesale revenue $/s)
*/
export function getProductOutputRates(p, pState, state) {
if (!pState || !pState.unlocked || !pState.lines || pState.lines <= 0) {
return { prodRate: 0, revRate: 0, isBottlenecked: false };
}
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
const roboticsMult = state.upgrades?.automated_robotics ? 2.0 : 1.0;
const baseCycleTime = Math.max(0.1, p.baseCycleTime / (milestoneMult * roboticsMult));
const isBottlenecked = !!pState.isBottlenecked;
// Output velocity: units per second
const prodRate = isBottlenecked ? 0 : (1 / baseCycleTime) * p.outputAmount * pState.lines;
// Logistics revenue rate: items sold passively per second * price
const logisticsUnits = pState.logistics || 0;
const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0;
const salePrice = p.basePrice * logisticsPriceMult;
const maxSellCapacity = logisticsUnits * 0.75;
// If stockpiled, sells at max fleet capacity; otherwise bottlenecked by production velocity
const effectiveSellRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : prodRate);
const revRate = effectiveSellRate * salePrice;
return { prodRate, revRate, isBottlenecked, milestoneMult };
}
/**
* Computes global net supply chain flow (Production - Downstream Manufacturing Consumption - Logistics Sales)
*/
export function computeProductNetFlow(product, state) {
if (!state.products || !state.products[product.id]?.unlocked) {
return { netRate: 0, prodRate: 0, consumedRate: 0, soldRate: 0 };
}
const pState = state.products[product.id];
const { prodRate, isBottlenecked } = getProductOutputRates(product, pState, state);
const effectiveProd = isBottlenecked ? 0 : prodRate;
// Calculate how much active downstream factories consume per second
let consumedRate = 0;
// Import PRODUCTS dynamically from caller or check through global/state if needed
// In our architecture, caller passes or we compute from state.products and dependencies
const logisticsUnits = pState.logistics || 0;
const maxSellCapacity = logisticsUnits * 0.75;
const soldRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : effectiveProd);
return {
prodRate: effectiveProd,
soldRate,
netRate: effectiveProd - soldRate
};
}

View file

@ -0,0 +1,162 @@
// Production UI: Sector Toolbar and Executive Summaries
import { SECTORS } from '../../data/products.js';
import { fmtNum } from '../../core/formatters.js';
export function renderSectorBarHtml(selectedSector, engine) {
const currentSectorObj = SECTORS.find(s => s.id === selectedSector);
let html = `
<div class="production-toolbar">
<div class="sector-tabs-scroll">
<button class="sector-pill ${selectedSector === 'all' ? 'active' : ''}" data-sector="all" title="All Sectors">
<iconify-icon icon="lucide:layout-grid"></iconify-icon>
<span class="sector-pill-rate font-mono" id="pill-rate-all">+$${fmtNum(engine?.cashRate || 0, 1)}/s</span>
</button>
${SECTORS.map(s => {
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 };
return `
<button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}" title="${s.name}">
<iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon>
<span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 1)}/s</span>
</button>
`;
}).join('')}
</div>
</div>
`;
// 1. Single 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. All Sectors Mini Summary Grid
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>
`;
}
return html;
}
export function updateSectorBarDom(selectedSector, engine) {
const elPillRateAll = document.getElementById('pill-rate-all');
if (elPillRateAll) {
const text = `+$${fmtNum(engine?.cashRate || 0, 1)}/s`;
if (elPillRateAll.textContent !== text) elPillRateAll.textContent = text;
}
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, 1)}/s`;
if (elPillRate.textContent !== text) elPillRate.textContent = text;
}
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;
}
}
}
}
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;
}
}
}
}

View file

@ -1,57 +1,10 @@
// Production - The Idle Game: Production Floor View // Production - The Idle Game: Production Floor View
// High-performance fine-grained DOM mounting & updating with comprehensive sector output metrics // Clean modular orchestrator connecting sector toolbar, facility cards, and fine-grained DOM updating
import { PRODUCTS, SECTORS, getProduct, getNextMilestone, getUnitMilestoneMultiplier } from '../data/products.js'; import { PRODUCTS } from '../data/products.js';
import { fmtCurrency, fmtNum } from '../core/formatters.js'; import { renderSectorBarHtml, updateSectorBarDom } from './production/sectorBar.js';
import { renderFacilityCardHtml, updateFacilityCardDom } from './production/facilityCard.js';
export function calculateMultiCost(baseCost, currentCount, multiplier) { export { calculateMultiCost, getProductOutputRates } from './production/ratesCalculator.js';
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;
}
let totalCost = 0;
for (let i = 0; i < countToBuy; i++) {
totalCost += baseCost * Math.pow(growth, currentCount + i);
}
return { count: countToBuy, cost: totalCost };
}
/**
* Computes live individual factory output rates (units/s and wholesale revenue $/s)
*/
export function getProductOutputRates(p, pState, state) {
if (!pState || !pState.unlocked || !pState.lines || pState.lines <= 0) {
return { prodRate: 0, revRate: 0, isBottlenecked: false };
}
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
const roboticsMult = state.upgrades?.automated_robotics ? 2.0 : 1.0;
const baseCycleTime = Math.max(0.1, p.baseCycleTime / (milestoneMult * roboticsMult));
const isBottlenecked = !!pState.isBottlenecked;
// Output velocity: units per second
const prodRate = isBottlenecked ? 0 : (1 / baseCycleTime) * p.outputAmount * pState.lines;
// Logistics revenue rate: items sold passively per second * price
const logisticsUnits = pState.logistics || 0;
const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0;
const salePrice = p.basePrice * logisticsPriceMult;
const maxSellCapacity = logisticsUnits * 0.75;
// If stockpiled, sells at max fleet capacity; otherwise bottlenecked by production velocity
const effectiveSellRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : prodRate);
const revRate = effectiveSellRate * salePrice;
return { prodRate, revRate, isBottlenecked, milestoneMult };
}
/** /**
* Mounts the production floor DOM structure ONCE * Mounts the production floor DOM structure ONCE
@ -59,268 +12,18 @@ export function getProductOutputRates(p, pState, state) {
export function mountProductionFloor(container, state, onAction, engine) { 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;
return p.sector === selectedSector; return p.sector === selectedSector;
}); });
let html = ` let html = renderSectorBarHtml(selectedSector, engine);
<div class="production-toolbar">
<div class="sector-tabs-scroll">
<button class="sector-pill ${selectedSector === 'all' ? 'active' : ''}" data-sector="all" title="All Sectors">
<iconify-icon icon="lucide:layout-grid"></iconify-icon>
<span class="sector-pill-rate font-mono" id="pill-rate-all">+$${fmtNum(engine?.cashRate || 0, 1)}/s</span>
</button>
${SECTORS.map(s => {
const stats = engine?.sectorStats?.[s.id] || { revenueRate: 0 };
return `
<button class="sector-pill ${selectedSector === s.id ? 'active' : ''}" data-sector="${s.id}" title="${s.name}">
<iconify-icon icon="${s.icon}" style="color: ${s.color};"></iconify-icon>
<span class="sector-pill-rate font-mono" id="pill-rate-${s.id}">+$${fmtNum(stats.revenueRate, 1)}/s</span>
</button>
`;
}).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">`; 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 };
const sector = SECTORS.find(s => s.id === p.sector); html += renderFacilityCardHtml(p, pState, state, buyMode);
const isSelected = state.selectedProductId === p.id;
if (!pState.unlocked) {
const canAffordUnlock = state.cash >= p.unlockCost;
html += `
<div class="facility-card locked-card ${isSelected ? 'selected' : ''}" id="card-${p.id}" 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'} &bull; 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" id="btn-unlock-${p.id}" 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;
}
const nextM = getNextMilestone(pState.lines);
const rates = getProductOutputRates(p, pState, state);
const milestoneMult = rates.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;
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' : ''}" id="chip-${p.id}-${inp.id}" title="${inpProduct?.name}">
<iconify-icon icon="${inpProduct?.icon || 'lucide:box'}"></iconify-icon>
${inpProduct?.name}: <strong id="chip-val-${p.id}-${inp.id}">${fmtNum(currentStock, 0)}</strong> / ${fmtNum(neededPerLine, 0)}
</span>
`;
}).join('')}
</div>
</div>
`;
}
let statusBadge = '';
if (pState.isBottlenecked) {
statusBadge = `<span class="status-badge badge-warning" id="badge-${p.id}"><iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs</span>`;
} else if (pState.lines > 0) {
statusBadge = `<span class="status-badge badge-success" id="badge-${p.id}"><iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)</span>`;
} else {
statusBadge = `<span class="status-badge badge-idle" id="badge-${p.id}">Idle (0 Lines)</span>`;
}
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
html += `
<div class="facility-card active-card ${isSelected ? 'selected' : ''}" id="card-${p.id}" 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} &bull; Tier ${p.tier}
</span>
<div id="badge-wrap-${p.id}">${statusBadge}</div>
</div>
<h3 class="facility-name">${p.name}</h3>
<span class="facility-type-sub" id="type-sub-${p.id}">${p.facilityName} &bull; <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" id="progress-${p.id}" style="width: ${progressPct}%; background-color: ${p.color};"></div>
</div>
<!-- Stock, Velocity and Revenue Summary -->
<div class="facility-stats-grid">
<div class="stat-cell">
<span class="stat-label">Stockpile:</span>
<span class="stat-value font-mono"><strong id="stock-${p.id}">${fmtNum(pState.stock, 0)}</strong> ${p.unit}</span>
</div>
<div class="stat-cell">
<span class="stat-label">Production Output:</span>
<span class="stat-value font-mono ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}" id="rate-units-${p.id}">
${rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`}
</span>
</div>
<div class="stat-cell">
<span class="stat-label">Logistics Rev:</span>
<span class="stat-value font-mono text-profit" id="rate-rev-${p.id}">+$${fmtNum(rates.revRate, 2)}/s</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" title="Logistics fleets automatically sell stockpiled goods continuously onto the wholesale market (0.75 units/sec per fleet)">
<span class="stat-label">Logistics Auto-Sell:</span>
<span class="stat-value font-mono" id="logistics-${p.id}">${pState.logistics || 0} fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)}/s)</span>
</div>
<div class="stat-cell">
<span class="stat-label">Milestone Eff:</span>
<span class="stat-value font-mono" id="eff-${p.id}">${milestoneMult}x Speed</span>
</div>
</div>
${inputsHtml}
<!-- Facility Actions Bar -->
<div class="facility-actions-row">
<button class="btn-corp-action btn-cycle" data-product="${p.id}" title="Run 1 manual production cycle">
<iconify-icon icon="lucide:play"></iconify-icon>
<span>Run Batch</span>
</button>
<button class="btn-primary-corp btn-expand" id="btn-expand-${p.id}" data-product="${p.id}" ${canAffordBuy ? '' : 'disabled'} title="Add more production lines to increase output volume">
<iconify-icon icon="lucide:plus-circle"></iconify-icon>
<span id="btn-expand-text-${p.id}">+${calc.count} Lines (${fmtCurrency(calc.cost)})</span>
</button>
${!pState.manager ? `
<button class="btn-corp-secondary btn-hire-mgr" id="btn-hire-${p.id}" data-product="${p.id}" ${canAffordManager ? '' : 'disabled'} title="Hires a Facility Lead who automatically runs production continuously without clicking">
<iconify-icon icon="lucide:user-check"></iconify-icon>
<span>Hire Lead (${fmtCurrency(managerCost)})</span>
</button>
` : `
<span class="mgr-hired-badge" title="Facility Lead hired: 100% automated continuous production active">
<iconify-icon icon="lucide:bot"></iconify-icon> <span>Lead Active (Auto)</span>
</span>
`}
<button class="btn-corp-secondary btn-expand-logistics" id="btn-logistics-${p.id}" data-product="${p.id}" ${canAffordLogistics ? '' : 'disabled'} title="Deploys an automated distribution fleet (+0.75 units/sec passive wholesale sales)">
<iconify-icon icon="lucide:truck"></iconify-icon>
<span id="btn-logistics-text-${p.id}">+Fleet (${fmtCurrency(logisticsCost)})</span>
</button>
</div>
</div>
`;
} }
html += `</div>`; html += `</div>`;
@ -368,214 +71,15 @@ export function mountProductionFloor(container, state, onAction, engine) {
*/ */
export function updateProductionFloor(container, state, engine) { export function updateProductionFloor(container, state, engine) {
const buyMode = state.buyMode || '1'; const buyMode = state.buyMode || '1';
const cash = state.cash;
const selectedSector = state.selectedSector || 'all'; const selectedSector = state.selectedSector || 'all';
// 1. Update Sector Navigation Pills Output Rates // 1. Update Sector Navigation Pills & Summaries
const elPillRateAll = document.getElementById('pill-rate-all'); updateSectorBarDom(selectedSector, engine);
if (elPillRateAll) {
const text = `+$${fmtNum(engine?.cashRate || 0, 1)}/s`;
if (elPillRateAll.textContent !== text) elPillRateAll.textContent = text;
}
for (const s of SECTORS) { // 2. Update Facility Cards
const stats = engine?.sectorStats?.[s.id];
if (stats) {
const elPillRate = document.getElementById(`pill-rate-${s.id}`);
if (elPillRate) {
const text = `+$${fmtNum(stats.revenueRate, 1)}/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;
updateFacilityCardDom(p, pState, state, buyMode);
if (!pState.unlocked) {
const btnUnlock = document.getElementById(`btn-unlock-${p.id}`);
if (btnUnlock) {
const canAfford = cash >= p.unlockCost;
if (btnUnlock.disabled !== !canAfford) {
btnUnlock.disabled = !canAfford;
}
}
continue;
}
// Live Individual Factory Rates & Output
const rates = getProductOutputRates(p, pState, state);
const elRateUnits = document.getElementById(`rate-units-${p.id}`);
if (elRateUnits) {
const unitsText = rates.isBottlenecked ? '0.0 (Paused)' : `${fmtNum(rates.prodRate, 1)}/s`;
if (elRateUnits.textContent !== unitsText) {
elRateUnits.textContent = unitsText;
}
elRateUnits.className = `stat-value font-mono ${rates.isBottlenecked ? 'text-warning' : 'text-profit'}`;
}
const elRateRev = document.getElementById(`rate-rev-${p.id}`);
if (elRateRev) {
const revText = `+$${fmtNum(rates.revRate, 2)}/s`;
if (elRateRev.textContent !== revText) {
elRateRev.textContent = revText;
}
}
const elEff = document.getElementById(`eff-${p.id}`);
if (elEff) {
const effText = `${rates.milestoneMult}x Speed`;
if (elEff.textContent !== effText) {
elEff.textContent = effText;
}
}
const elLogistics = document.getElementById(`logistics-${p.id}`);
if (elLogistics) {
const count = pState.logistics || 0;
const logText = `${count} fleets (${fmtNum(count * 0.75, 2)}/s)`;
if (elLogistics.textContent !== logText) {
elLogistics.textContent = logText;
}
}
const elProgress = document.getElementById(`progress-${p.id}`);
if (elProgress) {
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
elProgress.style.width = `${progressPct}%`;
}
// Stockpile Text
const elStock = document.getElementById(`stock-${p.id}`);
if (elStock) {
const stockText = fmtNum(pState.stock, 0);
if (elStock.textContent !== stockText) {
elStock.textContent = stockText;
}
}
// Status Badge
const elBadge = document.getElementById(`badge-${p.id}`);
if (elBadge) {
const isBottlenecked = pState.isBottlenecked;
const hasWarning = elBadge.classList.contains('badge-warning');
if (isBottlenecked && !hasWarning) {
elBadge.className = 'status-badge badge-warning';
elBadge.innerHTML = `<iconify-icon icon="lucide:alert-triangle"></iconify-icon> Waiting for Inputs`;
} else if (!isBottlenecked && hasWarning) {
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
elBadge.className = 'status-badge badge-success';
elBadge.innerHTML = `<iconify-icon icon="lucide:check-circle"></iconify-icon> Operating (${milestoneMult}x Speed)`;
}
}
// Input Chips Stock
if (p.inputs && p.inputs.length > 0) {
for (const inp of p.inputs) {
const currentStock = state.products[inp.id]?.stock || 0;
const elChipVal = document.getElementById(`chip-val-${p.id}-${inp.id}`);
if (elChipVal) {
const textVal = fmtNum(currentStock, 0);
if (elChipVal.textContent !== textVal) {
elChipVal.textContent = textVal;
}
}
const elChip = document.getElementById(`chip-${p.id}-${inp.id}`);
if (elChip) {
const needed = inp.amount * pState.lines;
const isLow = currentStock < needed;
elChip.classList.toggle('low-stock', isLow);
}
}
}
// Expand Button disabled state
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode);
const elBtnExpand = document.getElementById(`btn-expand-${p.id}`);
if (elBtnExpand) {
const canAfford = cash >= calc.cost;
if (elBtnExpand.disabled !== !canAfford) {
elBtnExpand.disabled = !canAfford;
}
}
// Hire Manager disabled state
const elBtnHire = document.getElementById(`btn-hire-${p.id}`);
if (elBtnHire) {
const canAfford = cash >= p.baseManagerCost;
if (elBtnHire.disabled !== !canAfford) {
elBtnHire.disabled = !canAfford;
}
}
// 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) {
const canAfford = cash >= logisticsCost;
if (elBtnLogistics.disabled !== !canAfford) {
elBtnLogistics.disabled = !canAfford;
}
const elBtnLogisticsText = document.getElementById(`btn-logistics-text-${p.id}`);
if (elBtnLogisticsText) {
const text = `+Fleet (${fmtCurrency(logisticsCost)})`;
if (elBtnLogisticsText.textContent !== text) {
elBtnLogisticsText.textContent = text;
}
}
}
// Selected Card class
const elCard = document.getElementById(`card-${p.id}`);
if (elCard) {
const isSelected = state.selectedProductId === p.id;
elCard.classList.toggle('selected', isSelected);
}
} }
} }