Refactor large files into modular architecture: split CSS, products, and production floor UI
This commit is contained in:
parent
032b0cc255
commit
f94d7df8f8
21 changed files with 3065 additions and 2966 deletions
62
frontend/css/base/theme.css
Normal file
62
frontend/css/base/theme.css
Normal 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);
|
||||
}
|
||||
|
||||
193
frontend/css/layout/header.css
Normal file
193
frontend/css/layout/header.css
Normal 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);
|
||||
}
|
||||
|
||||
234
frontend/css/layout/responsive.css
Normal file
234
frontend/css/layout/responsive.css
Normal 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);
|
||||
}
|
||||
318
frontend/css/layout/workspace.css
Normal file
318
frontend/css/layout/workspace.css
Normal 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
93
frontend/css/views/achievements.css
Normal file
93
frontend/css/views/achievements.css
Normal 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);
|
||||
}
|
||||
|
||||
213
frontend/css/views/boardroom.css
Normal file
213
frontend/css/views/boardroom.css
Normal 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;
|
||||
}
|
||||
|
||||
184
frontend/css/views/contracts.css
Normal file
184
frontend/css/views/contracts.css
Normal 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);
|
||||
}
|
||||
|
||||
207
frontend/css/views/inspector.css
Normal file
207
frontend/css/views/inspector.css
Normal 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);
|
||||
}
|
||||
|
||||
395
frontend/css/views/production.css
Normal file
395
frontend/css/views/production.css
Normal 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;
|
||||
}
|
||||
|
||||
|
|
@ -1,569 +1,29 @@
|
|||
// 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 = [
|
||||
{
|
||||
id: 'agriculture',
|
||||
name: 'Agriculture & Food',
|
||||
icon: 'lucide:wheat',
|
||||
color: '#16a34a', // Emerald Green
|
||||
description: 'Primary crops, livestock, milling, and commercial foodstuffs.'
|
||||
},
|
||||
{
|
||||
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.'
|
||||
}
|
||||
AGRICULTURE_SECTOR,
|
||||
MATERIALS_SECTOR,
|
||||
MINING_SECTOR,
|
||||
CHEMICALS_SECTOR,
|
||||
ELECTRONICS_SECTOR,
|
||||
HEAVY_INDUSTRY_SECTOR
|
||||
];
|
||||
|
||||
export const PRODUCTS = [
|
||||
// --- SECTOR 1: AGRICULTURE & FOOD ---
|
||||
{
|
||||
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.'
|
||||
},
|
||||
|
||||
// --- 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.'
|
||||
}
|
||||
...AGRICULTURE_PRODUCTS,
|
||||
...MATERIALS_PRODUCTS,
|
||||
...MINING_PRODUCTS,
|
||||
...CHEMICALS_PRODUCTS,
|
||||
...ELECTRONICS_PRODUCTS,
|
||||
...HEAVY_INDUSTRY_PRODUCTS
|
||||
];
|
||||
|
||||
// Helper to look up a product by ID
|
||||
|
|
|
|||
143
frontend/js/data/sectors/agriculture.js
Normal file
143
frontend/js/data/sectors/agriculture.js
Normal 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.'
|
||||
}
|
||||
];
|
||||
99
frontend/js/data/sectors/chemicals.js
Normal file
99
frontend/js/data/sectors/chemicals.js
Normal 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.'
|
||||
}
|
||||
];
|
||||
77
frontend/js/data/sectors/electronics.js
Normal file
77
frontend/js/data/sectors/electronics.js
Normal 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.'
|
||||
}
|
||||
];
|
||||
55
frontend/js/data/sectors/heavyIndustry.js
Normal file
55
frontend/js/data/sectors/heavyIndustry.js
Normal 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.'
|
||||
}
|
||||
];
|
||||
99
frontend/js/data/sectors/materials.js
Normal file
99
frontend/js/data/sectors/materials.js
Normal 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.'
|
||||
}
|
||||
];
|
||||
99
frontend/js/data/sectors/mining.js
Normal file
99
frontend/js/data/sectors/mining.js
Normal 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.'
|
||||
}
|
||||
];
|
||||
310
frontend/js/ui/production/facilityCard.js
Normal file
310
frontend/js/ui/production/facilityCard.js
Normal 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'} • 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} • 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} • <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);
|
||||
}
|
||||
}
|
||||
79
frontend/js/ui/production/ratesCalculator.js
Normal file
79
frontend/js/ui/production/ratesCalculator.js
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
162
frontend/js/ui/production/sectorBar.js
Normal file
162
frontend/js/ui/production/sectorBar.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +1,10 @@
|
|||
// 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 { fmtCurrency, fmtNum } from '../core/formatters.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 };
|
||||
}
|
||||
import { PRODUCTS } from '../data/products.js';
|
||||
import { renderSectorBarHtml, updateSectorBarDom } from './production/sectorBar.js';
|
||||
import { renderFacilityCardHtml, updateFacilityCardDom } from './production/facilityCard.js';
|
||||
export { calculateMultiCost, getProductOutputRates } from './production/ratesCalculator.js';
|
||||
|
||||
/**
|
||||
* Mounts the production floor DOM structure ONCE
|
||||
|
|
@ -59,268 +12,18 @@ export function getProductOutputRates(p, pState, state) {
|
|||
export function mountProductionFloor(container, state, onAction, engine) {
|
||||
const selectedSector = state.selectedSector || 'all';
|
||||
const buyMode = state.buyMode || '1';
|
||||
const currentSectorObj = SECTORS.find(s => s.id === selectedSector);
|
||||
|
||||
const filteredProducts = PRODUCTS.filter(p => {
|
||||
if (selectedSector === 'all') return true;
|
||||
return p.sector === selectedSector;
|
||||
});
|
||||
|
||||
let html = `
|
||||
<div class="production-toolbar">
|
||||
<div class="sector-tabs-scroll">
|
||||
<button class="sector-pill ${selectedSector === 'all' ? 'active' : ''}" data-sector="all" 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>
|
||||
`;
|
||||
}
|
||||
|
||||
let html = renderSectorBarHtml(selectedSector, engine);
|
||||
html += `<div class="production-grid">`;
|
||||
|
||||
for (const p of filteredProducts) {
|
||||
const pState = state.products[p.id] || { unlocked: false, lines: 0, stock: 0, cycleProgress: 0, isBottlenecked: false };
|
||||
const sector = SECTORS.find(s => s.id === p.sector);
|
||||
const isSelected = state.selectedProductId === p.id;
|
||||
|
||||
if (!pState.unlocked) {
|
||||
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'} • 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} • 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} • <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 += renderFacilityCardHtml(p, pState, state, buyMode);
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
|
@ -368,214 +71,15 @@ export function mountProductionFloor(container, state, onAction, engine) {
|
|||
*/
|
||||
export function updateProductionFloor(container, state, engine) {
|
||||
const buyMode = state.buyMode || '1';
|
||||
const cash = state.cash;
|
||||
const selectedSector = state.selectedSector || 'all';
|
||||
|
||||
// 1. Update Sector Navigation Pills Output Rates
|
||||
const elPillRateAll = document.getElementById('pill-rate-all');
|
||||
if (elPillRateAll) {
|
||||
const text = `+$${fmtNum(engine?.cashRate || 0, 1)}/s`;
|
||||
if (elPillRateAll.textContent !== text) elPillRateAll.textContent = text;
|
||||
}
|
||||
// 1. Update Sector Navigation Pills & Summaries
|
||||
updateSectorBarDom(selectedSector, engine);
|
||||
|
||||
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;
|
||||
}
|
||||
// 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
|
||||
// 2. Update Facility Cards
|
||||
for (const p of PRODUCTS) {
|
||||
const pState = state.products[p.id];
|
||||
if (!pState) continue;
|
||||
|
||||
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);
|
||||
}
|
||||
updateFacilityCardDom(p, pState, state, buyMode);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue