feat(arch): implement single-source-of-truth modular sector registry and DAG validator for scaling to 100+ products

This commit is contained in:
Christian 2026-10-16 16:45:42 +02:00
parent 98fb1147b3
commit 7cdcd4fb5e
6 changed files with 403 additions and 152 deletions

166
PRODUCT_GENERATOR_GUIDE.md Normal file
View file

@ -0,0 +1,166 @@
# Plug-and-Play Production Unit & Sector Generator Guide
This guide describes how to add **5, 10, 50, or 100+** new production units, commodities, and industrial sectors to **Production: The Idle Game**.
The game uses a **Single-Source-of-Truth** architecture:
* Adding or modifying a sector file in `frontend/js/data/sectors/<sectorName>.js` **automatically** updates both the frontend UI and the Python authoritative simulation backend.
* You never need to touch engine code, database schemas, or calculation formulas!
---
## 1. Quick-Start: Adding a New Product to an Existing Sector
To add a new product to an existing sector (e.g. `frontend/js/data/sectors/chemicals.js`):
Simply copy and append this block to the sector's `PRODUCTS` array:
```javascript
{
id: 'bio_fertilizer', // Unique snake_case string ID
name: 'Bio-Organic Fertilizer', // User-facing commodity name
sector: 'chemicals', // Parent sector ID ('agriculture', 'chemicals', etc.)
tier: 2, // Industrial Tier (1, 2, 3, 4+)
icon: 'lucide:sprout', // Lucide or Iconify icon name
color: '#10b981', // Hex accent color
unit: 'barrels', // Measurement unit (e.g. 'bushels', 'ingots', 'crates')
// DAG Supply Chain Inputs (Leave empty [] for raw extraction)
inputs: [
{ id: 'sugar_beet', amount: 2 },
{ id: 'polymers', amount: 1 }
],
outputAmount: 1, // Yield per batch
baseCycleTime: 3.2, // Raw cycle duration in seconds
// Capital Investment & Economics
unlockCost: 65000, // Cost to acquire the industrial permit
unlockedDefault: false, // True only for starter tier 1 units
baseFacilityCost: 45000, // Base line cost (scales exponentially by 1.15^lines)
baseManagerCost: 110000, // Cost to hire automating Operations Lead
baseLogisticsCost: 80000, // Cost to commission 1st wholesale logistics fleet
basePrice: 95.00, // Base wholesale market spot price
// Immersive Corporate Metadata
facilityName: 'Bio-Synthesis Complex', // Name of the physical production building
managerName: 'Bio-Process Engineer', // Title of the facility operations lead
logisticsName: 'Sealed Tanker Fleet', // Name of the dedicated logistics fleet
description: 'Eco-synthesized agricultural nutrients formulated from organic biomass.'
}
```
---
## 2. Quick-Start: Adding a Completely New Sector
To add a brand new industrial sector (e.g. `Biotechnology & Pharmaceuticals`):
### Step A: Create `frontend/js/data/sectors/biotech.js`
```javascript
// Sector: Biotechnology & Pharmaceuticals
export const BIOTECH_SECTOR = {
id: 'biotech',
name: 'Biotech & Pharma',
icon: 'lucide:dna',
color: '#8b5cf6',
description: 'Advanced genomics, clinical pharmaceuticals, and synthetic biochemistry.'
};
export const BIOTECH_PRODUCTS = [
{
id: 'cell_culture',
name: 'Cellular Cultures',
sector: 'biotech',
tier: 1,
icon: 'lucide:microscope',
color: '#a78bfa',
unit: 'vials',
inputs: [], // Raw bio-harvesting
outputAmount: 1,
baseCycleTime: 1.5,
unlockCost: 50000,
unlockedDefault: false,
baseFacilityCost: 35000,
baseManagerCost: 90000,
baseLogisticsCost: 60000,
basePrice: 65.00,
facilityName: 'Bio-Incubator Lab',
managerName: 'Chief Microbiologist',
logisticsName: 'Cryo-Courier Unit',
description: 'Living cell cultures cultivated in sterile bioreactor vessels.'
},
{
id: 'synthetic_insulin',
name: 'Synthetic Pharmaceuticals',
sector: 'biotech',
tier: 2,
icon: 'lucide:pill',
color: '#c084fc',
unit: 'doses',
inputs: [
{ id: 'cell_culture', amount: 2 },
{ id: 'refined_sugar', amount: 1 }
],
outputAmount: 1,
baseCycleTime: 3.5,
unlockCost: 280000,
unlockedDefault: false,
baseFacilityCost: 220000,
baseManagerCost: 450000,
baseLogisticsCost: 320000,
basePrice: 380.00,
facilityName: 'Pharmaceutical Synthesis Line',
managerName: 'Clinical Director',
logisticsName: 'Cold-Chain Express',
description: 'Purified medicinal formulations synthesized for global hospital networks.'
}
];
```
### Step B: Register the Sector in `frontend/js/data/products.js`
Open `frontend/js/data/products.js` and add 2 lines:
```javascript
import { BIOTECH_SECTOR, BIOTECH_PRODUCTS } from './sectors/biotech.js';
export const SECTORS = [
...,
BIOTECH_SECTOR
];
export const PRODUCTS = [
...,
...BIOTECH_PRODUCTS
];
```
---
## 3. Automated Verification Tool: `validate_dag.py`
Whenever you add new products or sectors, run the built-in integrity validator:
```bash
python backend/validate_dag.py
```
This validator checks:
1. **Acyclic Invariant:** Confirms there are zero circular dependency loops (e.g. A requires B, B requires A).
2. **Missing Reference Guard:** Confirms every `inputs[].id` exists in the catalog.
3. **Economic Balance Check:** Confirms that a product's output wholesale revenue exceeds the cost of its raw input materials.
4. **Tier Consistency:** Checks that products don't accidentally consume items from higher tiers.
---
## 4. Visual Asset Generation Prompt Recipe
For every product added, generate matching visuals and place them in the assets folders:
* **Product Specimen Photo:** `frontend/assets/images/products/{id}_product.jpg`
* **Facility Blueprint Photo:** `frontend/assets/images/facilities/{id}_facility.jpg`
### AI Image Generator Prompt Template:
> **Product Prompt:**
> `Commercial studio macro photograph of {Product Name} ({Unit}). Isolated on a dark matte slate workbench, soft directional studio rim lighting, razor-sharp focus on material texture, hyper-detailed, Hasselblad H6D-100c 120mm macro lens, 8k.`
>
> **Facility Prompt:**
> `Cinematic wide-angle architectural exterior photo of a high-tech {Facility Name}. Modern industrial engineering, stainless steel pipes, clean structural geometry, volumetric twilight lighting, subtle atmospheric steam, National Geographic documentary style, 35mm tilt-shift lens.`
*Note: If image files are not yet created, the game gracefully displays crisp SVG icons and styled placeholders without breaking!*

79
backend/catalog_loader.py Normal file
View file

@ -0,0 +1,79 @@
"""
Production - The Idle Game: Sector & Product Registry Loader
Parses modular JS sector files into authoritative Python dictionaries.
Ensures single-source-of-truth between frontend definitions and backend simulation.
"""
import os
import glob
import json
import re
def js_to_dict(js_code: str):
"""Converts a standard JavaScript object/array literal to a Python dict/list."""
# Remove single-line comments
clean = re.sub(r'//.*', '', js_code)
# Quote unquoted property keys: e.g. id: 'wheat' -> "id": "wheat"
clean = re.sub(r'([{,]\s*)([a-zA-Z0-9_]+)\s*:', r'\1"\2":', clean)
# Replace single quotes with double quotes
clean = clean.replace("'", '"')
# Remove trailing commas before } or ]
clean = re.sub(r',\s*([}\]])', r'\1', clean)
return json.loads(clean)
def get_sectors_directory() -> str:
"""Finds the directory containing sector .js files whether in local dev or Docker."""
candidates = [
os.path.join(os.path.dirname(__file__), "..", "frontend", "js", "data", "sectors"), # Local repo dev
os.path.join(os.path.dirname(__file__), "sectors"), # Docker mount /app/sectors
"/app/sectors", # Standard container mount
os.path.abspath("frontend/js/data/sectors")
]
for c in candidates:
if os.path.isdir(c) and glob.glob(os.path.join(c, "*.js")):
return os.path.abspath(c)
return ""
def load_all_sectors_and_products():
"""
Scans the sectors directory, parsing all modular JS sector files.
Returns:
(sectors_dict, products_dict)
"""
sec_dir = get_sectors_directory()
sectors = {}
products = {}
if not sec_dir:
print("[WARNING] No sector files directory found! Falling back to empty catalog.")
return sectors, products
for file_path in glob.glob(os.path.join(sec_dir, "*.js")):
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Parse Sector Object
sec_match = re.search(r'export\s+const\s+[A-Z_]+_SECTOR\s*=\s*({[\s\S]*?});', content)
if sec_match:
sec_obj = js_to_dict(sec_match.group(1))
sectors[sec_obj["id"]] = sec_obj
# Parse Products Array
prod_match = re.search(r'export\s+const\s+[A-Z_]+_PRODUCTS\s*=\s*(\[[\s\S]*?\]);', content)
if prod_match:
prod_list = js_to_dict(prod_match.group(1))
for p in prod_list:
products[p["id"]] = p
except Exception as e:
print(f"[ERROR] Failed to parse sector file {file_path}: {e}")
return sectors, products
if __name__ == '__main__':
s, p = load_all_sectors_and_products()
print(f"Loaded {len(s)} sectors and {len(p)} products.")
for sid, sobj in s.items():
count = sum(1 for prod in p.values() if prod.get('sector') == sid)
print(f" - {sobj['name']} ({sid}): {count} commodities")

View file

@ -5,159 +5,15 @@ Authoritative offline tick progression, multi-tier DAG supply chains, and action
import math import math
import time import time
from catalog_loader import load_all_sectors_and_products
# Initial Product Catalog configuration mirroring frontend/js/data/products.js # Dynamically loaded Product & Sector Registry from modular sector files
PRODUCT_CONFIGS = { SECTOR_CONFIGS, PRODUCT_CONFIGS = load_all_sectors_and_products()
# Sector 1: Agriculture & Food
'wheat': {
'sector': 'agriculture', 'tier': 1, 'inputs': [],
'outputAmount': 1, 'baseCycleTime': 1.0, 'unlockCost': 0,
'baseFacilityCost': 15, 'baseManagerCost': 100, 'baseLogisticsCost': 80,
'basePrice': 1.50, 'unlockedDefault': True
},
'flour': {
'sector': 'agriculture', 'tier': 2, 'inputs': [{'id': 'wheat', 'amount': 2}],
'outputAmount': 1, 'baseCycleTime': 1.8, 'unlockCost': 250,
'baseFacilityCost': 200, 'baseManagerCost': 850, 'baseLogisticsCost': 600,
'basePrice': 6.20, 'unlockedDefault': False
},
'sugar_beet': {
'sector': 'agriculture', 'tier': 1, 'inputs': [],
'outputAmount': 1, 'baseCycleTime': 1.5, 'unlockCost': 650,
'baseFacilityCost': 550, 'baseManagerCost': 1800, 'baseLogisticsCost': 1200,
'basePrice': 4.80, 'unlockedDefault': False
},
'refined_sugar': {
'sector': 'agriculture', 'tier': 2, 'inputs': [{'id': 'sugar_beet', 'amount': 3}],
'outputAmount': 1, 'baseCycleTime': 2.2, 'unlockCost': 2200,
'baseFacilityCost': 1800, 'baseManagerCost': 4500, 'baseLogisticsCost': 3200,
'basePrice': 22.00, 'unlockedDefault': False
},
'fresh_bread': {
'sector': 'agriculture', 'tier': 3, 'inputs': [{'id': 'flour', 'amount': 2}, {'id': 'refined_sugar', 'amount': 1}],
'outputAmount': 2, 'baseCycleTime': 3.0, 'unlockCost': 9500,
'baseFacilityCost': 7500, 'baseManagerCost': 18000, 'baseLogisticsCost': 12500,
'basePrice': 58.00, 'unlockedDefault': False
},
'packaged_meals': {
'sector': 'agriculture', 'tier': 4, 'inputs': [{'id': 'fresh_bread', 'amount': 3}],
'outputAmount': 1, 'baseCycleTime': 4.5, 'unlockCost': 45000,
'baseFacilityCost': 36000, 'baseManagerCost': 75000, 'baseLogisticsCost': 55000,
'basePrice': 260.00, 'unlockedDefault': False
},
# Sector 2: Forestry & Materials def refresh_catalog():
'timber': { """Hot-reloads the product catalog if new sector files were added dynamically."""
'sector': 'materials', 'tier': 1, 'inputs': [], global SECTOR_CONFIGS, PRODUCT_CONFIGS
'outputAmount': 1, 'baseCycleTime': 1.2, 'unlockCost': 120, SECTOR_CONFIGS, PRODUCT_CONFIGS = load_all_sectors_and_products()
'baseFacilityCost': 95, 'baseManagerCost': 350, 'baseLogisticsCost': 240,
'basePrice': 2.80, 'unlockedDefault': False
},
'planks': {
'sector': 'materials', 'tier': 2, 'inputs': [{'id': 'timber', 'amount': 2}],
'outputAmount': 1, 'baseCycleTime': 2.0, 'unlockCost': 1200,
'baseFacilityCost': 950, 'baseManagerCost': 2800, 'baseLogisticsCost': 2000,
'basePrice': 11.50, 'unlockedDefault': False
},
'paper': {
'sector': 'materials', 'tier': 2, 'inputs': [{'id': 'timber', 'amount': 3}],
'outputAmount': 2, 'baseCycleTime': 2.5, 'unlockCost': 3500,
'baseFacilityCost': 2800, 'baseManagerCost': 6500, 'baseLogisticsCost': 4800,
'basePrice': 16.00, 'unlockedDefault': False
},
'furniture': {
'sector': 'materials', 'tier': 3, 'inputs': [{'id': 'planks', 'amount': 3}],
'outputAmount': 1, 'baseCycleTime': 3.8, 'unlockCost': 18000,
'baseFacilityCost': 14000, 'baseManagerCost': 35000, 'baseLogisticsCost': 26000,
'basePrice': 95.00, 'unlockedDefault': False
},
# Sector 3: Mining & Metallurgy
'iron_ore': {
'sector': 'mining', 'tier': 1, 'inputs': [],
'outputAmount': 1, 'baseCycleTime': 1.6, 'unlockCost': 1500,
'baseFacilityCost': 1200, 'baseManagerCost': 3500, 'baseLogisticsCost': 2400,
'basePrice': 12.00, 'unlockedDefault': False
},
'coal': {
'sector': 'mining', 'tier': 1, 'inputs': [],
'outputAmount': 1, 'baseCycleTime': 1.8, 'unlockCost': 2800,
'baseFacilityCost': 2200, 'baseManagerCost': 5500, 'baseLogisticsCost': 3800,
'basePrice': 15.00, 'unlockedDefault': False
},
'steel_ingots': {
'sector': 'mining', 'tier': 2, 'inputs': [{'id': 'iron_ore', 'amount': 2}, {'id': 'coal', 'amount': 1}],
'outputAmount': 1, 'baseCycleTime': 3.2, 'unlockCost': 12500,
'baseFacilityCost': 9800, 'baseManagerCost': 24000, 'baseLogisticsCost': 17000,
'basePrice': 72.00, 'unlockedDefault': False
},
'metal_frames': {
'sector': 'mining', 'tier': 3, 'inputs': [{'id': 'steel_ingots', 'amount': 3}],
'outputAmount': 1, 'baseCycleTime': 4.6, 'unlockCost': 65000,
'baseFacilityCost': 52000, 'baseManagerCost': 110000, 'baseLogisticsCost': 80000,
'basePrice': 320.00, 'unlockedDefault': False
},
# Sector 4: Chemicals & Polymers
'crude_oil': {
'sector': 'chemicals', 'tier': 1, 'inputs': [],
'outputAmount': 1, 'baseCycleTime': 2.0, 'unlockCost': 25000,
'baseFacilityCost': 20000, 'baseManagerCost': 48000, 'baseLogisticsCost': 35000,
'basePrice': 55.00, 'unlockedDefault': False
},
'silica_sand': {
'sector': 'chemicals', 'tier': 1, 'inputs': [],
'outputAmount': 1, 'baseCycleTime': 2.2, 'unlockCost': 45000,
'baseFacilityCost': 36000, 'baseManagerCost': 75000, 'baseLogisticsCost': 52000,
'basePrice': 85.00, 'unlockedDefault': False
},
'polymers': {
'sector': 'chemicals', 'tier': 2, 'inputs': [{'id': 'crude_oil', 'amount': 2}],
'outputAmount': 1, 'baseCycleTime': 3.5, 'unlockCost': 110000,
'baseFacilityCost': 88000, 'baseManagerCost': 175000, 'baseLogisticsCost': 120000,
'basePrice': 210.00, 'unlockedDefault': False
},
'silicon': {
'sector': 'chemicals', 'tier': 2, 'inputs': [{'id': 'silica_sand', 'amount': 2}],
'outputAmount': 1, 'baseCycleTime': 4.0, 'unlockCost': 220000,
'baseFacilityCost': 175000, 'baseManagerCost': 35000, 'baseLogisticsCost': 240000,
'basePrice': 420.00, 'unlockedDefault': False
},
# Sector 5: High-Tech & Electronics
'microchips': {
'sector': 'electronics', 'tier': 3, 'inputs': [{'id': 'silicon', 'amount': 1}, {'id': 'polymers', 'amount': 1}],
'outputAmount': 2, 'baseCycleTime': 4.8, 'unlockCost': 550000,
'baseFacilityCost': 440000, 'baseManagerCost': 850000, 'baseLogisticsCost': 600000,
'basePrice': 650.00, 'unlockedDefault': False
},
'circuit_boards': {
'sector': 'electronics', 'tier': 3, 'inputs': [{'id': 'microchips', 'amount': 2}, {'id': 'polymers', 'amount': 2}],
'outputAmount': 1, 'baseCycleTime': 5.5, 'unlockCost': 1500000,
'baseFacilityCost': 1200000, 'baseManagerCost': 2400000, 'baseLogisticsCost': 1600000,
'basePrice': 2200.00, 'unlockedDefault': False
},
'smartphones': {
'sector': 'electronics', 'tier': 4, 'inputs': [{'id': 'circuit_boards', 'amount': 1}, {'id': 'polymers', 'amount': 2}],
'outputAmount': 1, 'baseCycleTime': 6.5, 'unlockCost': 5000000,
'baseFacilityCost': 4000000, 'baseManagerCost': 8000000, 'baseLogisticsCost': 5500000,
'basePrice': 6800.00, 'unlockedDefault': False
},
# Sector 6: Heavy Industry & Vehicles
'engines': {
'sector': 'heavy_industry', 'tier': 3, 'inputs': [{'id': 'steel_ingots', 'amount': 3}, {'id': 'metal_frames', 'amount': 1}],
'outputAmount': 1, 'baseCycleTime': 6.0, 'unlockCost': 3500000,
'baseFacilityCost': 2800000, 'baseManagerCost': 5500000, 'baseLogisticsCost': 3800000,
'basePrice': 4200.00, 'unlockedDefault': False
},
'commercial_trucks': {
'sector': 'heavy_industry', 'tier': 4, 'inputs': [{'id': 'engines', 'amount': 1}, {'id': 'metal_frames', 'amount': 2}, {'id': 'circuit_boards', 'amount': 1}],
'outputAmount': 1, 'baseCycleTime': 8.5, 'unlockCost': 15000000,
'baseFacilityCost': 12000000, 'baseManagerCost': 22000000, 'baseLogisticsCost': 15000000,
'basePrice': 26500.00, 'unlockedDefault': False
}
}
UNIT_MILESTONES = [1, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000] UNIT_MILESTONES = [1, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000]
@ -444,7 +300,29 @@ def apply_actions(state: dict, actions: list) -> dict:
return state return state
def sanitize_state(raw: dict) -> dict: def sanitize_state(raw: dict) -> dict:
"""Ensures incoming state doesn't have NaN or negative numbers.""" """Ensures incoming state doesn't have NaN or negative numbers and dynamically populates new products."""
if not isinstance(raw, dict): if not isinstance(raw, dict):
return get_initial_state() return get_initial_state()
# Dynamic re-hydration: Ensure any new products added to the catalog exist in the player's saved state
if 'products' not in raw or not isinstance(raw['products'], dict):
raw['products'] = {}
for pid, p in PRODUCT_CONFIGS.items():
if pid not in raw['products']:
is_unlocked = p.get('unlockedDefault', False)
raw['products'][pid] = {
'unlocked': is_unlocked,
'stock': 0.0,
'lines': 1 if is_unlocked else 0,
'manager': False,
'logistics': 1 if is_unlocked else 0,
'logisticsEnabled': True,
'supplyDownstreamEnabled': True,
'productionTarget': 1.0,
'cycleProgress': 0.0,
'isBottlenecked': False,
'totalProduced': 0.0
}
return raw return raw

105
backend/validate_dag.py Normal file
View file

@ -0,0 +1,105 @@
"""
Production - The Idle Game: Automated DAG & Supply Chain Integrity Validator
Scans all sector manifests and verifies:
1. All input IDs reference existing, valid product IDs.
2. No circular / cyclic dependency loops exist (Directed Acyclic Graph invariant).
3. Economic margin scaling (Wholesale value > Raw input costs).
4. Tier ordering integrity (Downstream Tier >= Upstream Tier).
"""
import sys
import os
# Add backend directory to path
sys.path.insert(0, os.path.dirname(__file__))
from catalog_loader import load_all_sectors_and_products
def validate_dag():
sectors, products = load_all_sectors_and_products()
errors = []
warnings = []
print(f"=== Validating Supply Chain DAG Integrity ===")
print(f"Discovered: {len(sectors)} Sectors, {len(products)} Commodities / Facilities.\n")
if not products:
errors.append("FATAL: No products discovered in catalog!")
return errors, warnings
# 1. Existence and Key Verification
for pid, p in products.items():
# Check required fields
for field in ['id', 'name', 'sector', 'tier', 'baseCycleTime', 'basePrice', 'outputAmount', 'baseFacilityCost']:
if field not in p:
errors.append(f"Product '{pid}' is missing required field '{field}'.")
# Check sector exists
sec_id = p.get('sector')
if sec_id not in sectors:
errors.append(f"Product '{pid}' references unknown sector '{sec_id}'.")
# Check inputs exist
inputs = p.get('inputs', [])
input_cost = 0.0
for inp in inputs:
inp_id = inp.get('id')
amt = inp.get('amount', 1)
if inp_id not in products:
errors.append(f"Product '{pid}' requires nonexistent input '{inp_id}'.")
else:
parent = products[inp_id]
input_cost += parent.get('basePrice', 0.0) * amt
# Tier sanity check
if parent.get('tier', 1) > p.get('tier', 1):
warnings.append(f"Tier Inversion: '{pid}' (Tier {p.get('tier')}) consumes higher-tier product '{inp_id}' (Tier {parent.get('tier')}).")
# Economic margin check (Wholesale price vs input cost)
output_val = p.get('basePrice', 0.0) * p.get('outputAmount', 1)
if inputs and output_val <= input_cost:
warnings.append(f"Negative Profit Margin: '{pid}' batch produces ${output_val:.2f} but consumes ${input_cost:.2f} in inputs.")
# 2. Circular Dependency (Cycle Detection via DFS)
visited = {} # 0 = unvisited, 1 = visiting (in recursion stack), 2 = visited
def has_cycle(curr_id, path):
visited[curr_id] = 1 # Mark as currently visiting
curr_p = products.get(curr_id, {})
for inp in curr_p.get('inputs', []):
nxt_id = inp.get('id')
if nxt_id not in products:
continue
if visited.get(nxt_id, 0) == 1:
# Cycle found!
cycle_str = " -> ".join(path + [nxt_id])
errors.append(f"CIRCULAR DEPENDENCY DETECTED: {cycle_str}")
return True
if visited.get(nxt_id, 0) == 0:
if has_cycle(nxt_id, path + [nxt_id]):
return True
visited[curr_id] = 2 # Done with this branch
return False
for pid in products:
if visited.get(pid, 0) == 0:
has_cycle(pid, [pid])
return errors, warnings
if __name__ == '__main__':
errs, warns = validate_dag()
if warns:
print("[WARNINGS]:")
for w in warns:
print(f" [WARN] {w}")
print()
if errs:
print("[ERRORS FOUND]:")
for e in errs:
print(f" [FAIL] {e}")
print(f"\nDAG Validation FAILED with {len(errs)} error(s).")
sys.exit(1)
else:
print("SUCCESS: DAG VALIDATION PASSED. Supply Chain is acyclic, consistent, and healthy!")
sys.exit(0)

View file

@ -9,6 +9,7 @@ services:
- DATA_DIR=/app/data - DATA_DIR=/app/data
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./frontend/js/data/sectors:/app/sectors:ro
networks: networks:
- production-net - production-net

View file

@ -95,5 +95,27 @@ export const CHEMICALS_PRODUCTS = [
managerName: 'Materials Scientist', managerName: 'Materials Scientist',
logisticsName: 'Clean Ingot Carrier', logisticsName: 'Clean Ingot Carrier',
description: '99.999% pure electronic grade monocrystalline silicon.' description: '99.999% pure electronic grade monocrystalline silicon.'
},
{
id: 'industrial_fertilizer',
name: 'Industrial Fertilizer',
sector: 'chemicals',
tier: 2,
icon: 'lucide:flask-round',
color: '#10b981',
unit: 'barrels',
inputs: [{ id: 'crude_oil', amount: 1 }],
outputAmount: 2,
baseCycleTime: 2.8,
unlockCost: 75000,
unlockedDefault: false,
baseFacilityCost: 55000,
baseManagerCost: 120000,
baseLogisticsCost: 85000,
basePrice: 110.00,
facilityName: 'Fertilizer Synthesis Plant',
managerName: 'Synthesis Chemist',
logisticsName: 'Agri-Chemical Carrier',
description: 'High-nitrogen chemical nutrients accelerating large-scale agrarian yields.'
} }
]; ];