From bcf32f3ca7662e811a125ab196758d91fdc9814c Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 15 Oct 2026 21:29:51 +0200 Subject: [PATCH] feat: transform game into Production - The Idle Game with multi-tier supply chains, B2B contracts, IPO prestige, and responsive executive UI --- .env.example | 6 +- .gitignore | 2 +- README.md | 143 +- backend/auth.py | 2 +- backend/database.py | 2 +- backend/engine.py | 806 +++++----- backend/main.py | 4 +- backend/test_engine.py | 76 + docker-compose.yml | 8 +- frontend/css/style.css | 2261 ++++++++++++++++++---------- frontend/index.html | 393 +++-- frontend/js/api.js | 120 -- frontend/js/app.js | 432 ++++++ frontend/js/config.js | 584 ------- frontend/js/core/engine.js | 226 +++ frontend/js/core/formatters.js | 72 + frontend/js/core/state.js | 162 ++ frontend/js/core/sync.js | 117 ++ frontend/js/data/achievements.js | 44 + frontend/js/data/boardroom.js | 63 + frontend/js/data/contracts.js | 128 ++ frontend/js/data/products.js | 591 ++++++++ frontend/js/game.js | 2038 ------------------------- frontend/js/ui/achievementsView.js | 65 + frontend/js/ui/boardroomView.js | 117 ++ frontend/js/ui/contractsView.js | 112 ++ frontend/js/ui/inspectorView.js | 141 ++ frontend/js/ui/productionView.js | 243 +++ 28 files changed, 4659 insertions(+), 4299 deletions(-) create mode 100644 backend/test_engine.py delete mode 100644 frontend/js/api.js create mode 100644 frontend/js/app.js delete mode 100644 frontend/js/config.js create mode 100644 frontend/js/core/engine.js create mode 100644 frontend/js/core/formatters.js create mode 100644 frontend/js/core/state.js create mode 100644 frontend/js/core/sync.js create mode 100644 frontend/js/data/achievements.js create mode 100644 frontend/js/data/boardroom.js create mode 100644 frontend/js/data/contracts.js create mode 100644 frontend/js/data/products.js delete mode 100644 frontend/js/game.js create mode 100644 frontend/js/ui/achievementsView.js create mode 100644 frontend/js/ui/boardroomView.js create mode 100644 frontend/js/ui/contractsView.js create mode 100644 frontend/js/ui/inspectorView.js create mode 100644 frontend/js/ui/productionView.js diff --git a/.env.example b/.env.example index 7f26bac..8d5d248 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,9 @@ -# El Puro Configuration +# Production - The Idle Game Configuration # Port on host to expose the application HOST_PORT=8080 -# Public domain name (e.g. elpuro.example.com or localhost) +# Public domain name (e.g. localhost) DOMAIN_NAME=localhost # Secret key for JWT signing -SECRET_KEY=el_puro_secret_key_change_in_production_jwt_99_1_pure +SECRET_KEY=production_game_enterprise_jwt_secret_key_change_in_production diff --git a/.gitignore b/.gitignore index 986f6e6..06e3db1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .env # Database & Persistent storage -data/ +/data/ *.db *.sqlite *.sqlite3 diff --git a/README.md b/README.md index 725b05c..2c296a4 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,47 @@ -# EL PURO +# PRODUCTION - The Idle Game -> **"99.1% Pure. Zero Compromise."** +> **"Build the Ultimate Global Supply Chain Empire."** -**El Puro** er et mobil-optimeret incremental/idle-spil stylet som en hemmelig, krypteret burner phone (*SilkNet OS v3.2*). Spillet er skabt til hurtige mikro-sessioner (30 sekunder til 3 minutter), hvor du opbygger et undergrundsimperium fra en lille cannabis-plante til et globalt syntetisk kartel. +**Production - The Idle Game** is an expansive, strategic incremental/idle web game inspired by *Capitalism*, *Factorio-lite*, and modern idle tycoon simulators. Take command of a burgeoning enterprise, starting from primary extraction (wheat, timber, iron ore) and scaling into complex, multi-tiered industrial supply chains producing microchips, commercial trucks, consumer smartphones, and corporate office furniture. -Spillet kΓΈrer som en selvstΓ¦ndig **Docker Compose stack** med en lynhurtig Python FastAPI backend, SQLite database og en Nginx reverse-proxy. +The game is packaged as a lightweight, turnkey **Docker Compose stack** powered by a fast Python FastAPI backend with SQLite persistence and an Nginx reverse-proxy frontend. --- -## πŸ“± NΓΈglefunktioner +## 🌟 Key Features -* **Burner Phone Interface:** MΓΈrkt OLED-design med monospaced taktiske datafonte (`JetBrains Mono`), statuslinje med Police Heat meter og 4 touch-venlige bund-tabs: - * βš—οΈ **Lab:** Manuel hΓΈst/syntese, hyring af laboranter og opskriftsblanding. - * πŸ’Ό **Streets:** Dealer-netvΓ¦rk og **Designer Cocktails** (kombination af stoffer som f.eks. *Calvin Klein*, *Speedball* og *Candy Flip*). - * πŸ•ΈοΈ **Network (Talent-TrΓ¦):** Forgrenet spindelvΓ¦v med 3 distinkte stier: *Path of the Chemist*, *Path of the Kingpin* og *Path of the Smuggler*. - * 🏦 **Offshore:** Hvidvaskning af snavsede kontanter til **Clean Bonds** (Prestige-reset med permanente multiplikatorer). -* **10 Progressive Stof-Tiers:** Cannabis, Magic Mushrooms, Adderall, Meth (Blue Sky), MDMA, Ketamin, Kokain (Pure White), Crack, Heroin og Fentanyl. -* **Police Heat & Raids:** Hurtige quick-time hΓ¦ndelser, hvor du kan bestikke politiet, gemme dit lager eller lΓΈbe risikoen for 2x profit. -* **Simpelt Honeypot Brugersystem:** Kun brugernavn og password. Ingen e-mail, ingen 2FA. Inkluderer en usynlig honeypot-fΓ¦lde (`website_hp`), der automatisk afviser bot-registreringer. -* **Cloud & Offline Sync:** Spillet gemmer lokalt og synkroniserer automatisk til serveren hvert 5. sekund. +* **Real Supply Chain Dependencies (DAG Pipeline):** + * Higher-tier industrial goods require and consume physical stockpiles of lower-tier commodities. + * Real-time bottleneck warnings when upstream inputs run short. + * 6 Interconnected Industry Sectors: + 1. **Agriculture & Food:** Wheat, Milled Flour, Sugar Beet, Refined Sugar, Artisan Bread, Packaged Meals. + 2. **Forestry & Materials:** Raw Timber, Milled Planks, Industrial Paper, Office Furniture. + 3. **Mining & Metallurgy:** Iron Ore, Coking Coal, Structural Steel, Heavy Frameworks. + 4. **Chemicals & Polymers:** Crude Oil, Silica Quartz Sand, Industrial Polymers, Purified Silicon. + 5. **High-Tech & Electronics:** Silicon Microchips, PCB Motherboards, Consumer Smartphones. + 6. **Heavy Industry & Vehicles:** Industrial Engines, Commercial Logistics Trucks. +* **Dual Revenue Channels:** + * **Continuous Logistics Distribution:** Deploy dedicated logistics fleets to automatically wholesale products to regional distributors for continuous passive cashflow. + * **B2B Procurement Contracts Board:** Bid on time-sensitive procurement tenders from MegaCorps and industrial clients for massive capital injections and reputation. +* **Corporate IPO & Boardroom Prestige:** + * File for an **Initial Public Offering (IPO)** on Wall Street when your enterprise valuation reaches critical mass. + * Cash in for **Venture Shares ($V-Shares)** to unlock permanent perks on the Boardroom R&D Tech Tree (Automated ERP Routing, Industrial Robotics, Logistics Subsidies, Venture Capital Syndicate). +* **Responsive Executive UI:** + * **Desktop Command Station:** 3-column workstation layout (Departments & Sectors sidebar, main Production Floor, and live Supply Chain Inspector with instant spot-market liquidation). + * **Mobile Optimization:** Ergonomic single-column interface with bottom navigation tabs and thumb-friendly controls. +* **Cloud & Offline Sync:** + * Plays instantly as a Guest with local persistence. + * Optional lightweight account system (username + password with anti-bot honeypot protection). + * Authoritative offline tick engine: when you return, your automated lines calculate production and logistics sales up to 8 hours. --- -## πŸ› οΈ Arkitektur & Teknologistak +## πŸ› οΈ Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Browser / Mobil UI β”‚ - β”‚ (HTML5, CSS, JS) β”‚ + β”‚ Browser Client UI β”‚ + β”‚ (ES Modules, CSS3) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ HTTP / :HOST_PORT β–Ό @@ -40,90 +54,73 @@ Spillet kΓΈrer som en selvstΓ¦ndig **Docker Compose stack** med en lynhurtig Pyt β”‚ β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Python FastAPIβ”‚ - β”‚ β”‚ (JWT & Auth) β”‚ + β”‚ β”‚ (JWT & Engine)β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β–Ό [frontend/] [SQLite DB] - (/app/data/elpuro.db) + (/app/data/production.db) ``` --- -## πŸš€ Hurtig Start med Docker Compose +## πŸš€ Quick Start with Docker Compose -### 1. ForudsΓ¦tninger +### 1. Prerequisites * [Docker](https://docs.docker.com/get-docker/) & [Docker Compose](https://docs.docker.com/compose/) -### 2. Klon repository -```bash -git clone https://git.p20.dev/christian/el-puro.git -cd el-puro -``` - -### 3. Konfiguration (.env) -Kopier eksempelfilen og tilpas dine indstillinger: +### 2. Configuration (.env) +Create a `.env` file from the example: ```bash cp .env.example .env ``` -Indhold i `.env`: -```env -# Port pΓ₯ vΓ¦rtsmaskinen hvor spillet tilgΓ₯s -HOST_PORT=8080 - -# Offentligt domΓ¦nenavn (valgfrit til vhost-routing) -DOMAIN_NAME=localhost - -# Hemmelig nΓΈgle til JWT signering -SECRET_KEY=el_puro_secret_key_change_in_production_jwt_99_1_pure -``` - -### 4. Start stakken +### 3. Launch the Stack ```bash docker compose up -d --build ``` -Spillet er nu tilgΓ¦ngeligt i din browser pΓ₯: -πŸ‘‰ **`http://localhost:8080`** (eller den port du valgte i `.env`). +The game is live in your browser at: +πŸ‘‰ **`http://localhost:8080`** (or the port defined in `.env`). --- -## πŸ“ Projektstruktur +## πŸ“ Project Directory Structure ``` . -β”œβ”€β”€ .env.example # Skabelon til miljΓΈvariable -β”œβ”€β”€ docker-compose.yml # Docker Compose opsΓ¦tning -β”œβ”€β”€ backend/ # FastAPI backend +β”œβ”€β”€ .env.example +β”œβ”€β”€ docker-compose.yml +β”œβ”€β”€ backend/ β”‚ β”œβ”€β”€ Dockerfile β”‚ β”œβ”€β”€ requirements.txt -β”‚ β”œβ”€β”€ main.py # API endpoints & logik -β”‚ β”œβ”€β”€ auth.py # Bcrypt & JWT autentificering -β”‚ β”œβ”€β”€ database.py # SQLite databaseinitialisering -β”‚ └── models.py # Pydantic schemas (med honeypot trap) -β”œβ”€β”€ frontend/ # Mobil-optimeret frontend -β”‚ β”œβ”€β”€ index.html # SilkNet OS layout og modals +β”‚ β”œβ”€β”€ auth.py # JWT authentication & password hashing +β”‚ β”œβ”€β”€ database.py # SQLite connection and migration setup +β”‚ β”œβ”€β”€ engine.py # Authoritative DAG simulation and offline calculations +β”‚ β”œβ”€β”€ main.py # FastAPI endpoints +β”‚ └── models.py # Pydantic schemas +β”œβ”€β”€ frontend/ +β”‚ β”œβ”€β”€ index.html # Responsive executive layout β”‚ β”œβ”€β”€ css/ -β”‚ β”‚ └── style.css # Dark OLED tema og responsivt layout +β”‚ β”‚ └── style.css # Modern corporate design system β”‚ └── js/ -β”‚ β”œβ”€β”€ api.js # API klient & sky-synkronisering -β”‚ β”œβ”€β”€ config.js # 10 stoffer, opskrifter, skills og achievements -β”‚ └── game.js # Spilmotor, loop, heat og mechanics +β”‚ β”œβ”€β”€ app.js # Application orchestrator & game loop +β”‚ β”œβ”€β”€ core/ +β”‚ β”‚ β”œβ”€β”€ engine.js # Client-side DAG tick simulation +β”‚ β”‚ β”œβ”€β”€ formatters.js# Number notations (Standard K/M/B/T, Scientific) +β”‚ β”‚ β”œβ”€β”€ state.js # State manager, schema handling & IPO reset +β”‚ β”‚ └── sync.js # Backend synchronization & auth handling +β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”œβ”€β”€ products.js # 24+ commodities across 6 sectors +β”‚ β”‚ β”œβ”€β”€ contracts.js # B2B procurement tenders +β”‚ β”‚ β”œβ”€β”€ boardroom.js # IPO perks and Venture Share logic +β”‚ β”‚ └── achievements.js +β”‚ └── ui/ +β”‚ β”œβ”€β”€ productionView.js +β”‚ β”œβ”€β”€ contractsView.js +β”‚ β”œβ”€β”€ boardroomView.js +β”‚ β”œβ”€β”€ inspectorView.js +β”‚ └── achievementsView.js └── nginx/ - └── default.conf.template # Nginx reverse proxy konfiguration + └── default.conf.template ``` - ---- - -## πŸ”’ Honeypot Sikkerhed - -Registrerings-endpointet (`POST /api/auth/register`) inkluderer en skjult `website_hp` parameter. -* Menneskelige brugere ser ikke feltet og lader det stΓ₯ tomt. -* Automatiserede bots udfylder formularen blindt, hvorefter serveren afviser anmodningen med `400 Bad Request`. Dette holder databasen fri for falske konti uden behov for Captcha. - ---- - -## πŸ“œ Licens - -Privat projekt tilhΓΈrende Christian. Alle rettigheder forbeholdes. diff --git a/backend/auth.py b/backend/auth.py index 90b762c..948a7aa 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -5,7 +5,7 @@ from jose import jwt, JWTError from fastapi import HTTPException, Security, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -SECRET_KEY = os.getenv("SECRET_KEY", "el_puro_secret_key_change_in_production_jwt_99_1_pure") +SECRET_KEY = os.getenv("SECRET_KEY", "production_game_secret_key_change_in_production_jwt") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_DAYS = 365 diff --git a/backend/database.py b/backend/database.py index bb08230..0b8d94d 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2,7 +2,7 @@ import sqlite3 import os DB_DIR = os.getenv("DATA_DIR", "/app/data") -DB_PATH = os.path.join(DB_DIR, "elpuro.db") +DB_PATH = os.path.join(DB_DIR, "production.db") def get_db(): os.makedirs(DB_DIR, exist_ok=True) diff --git a/backend/engine.py b/backend/engine.py index 7beb52c..6cf4c13 100644 --- a/backend/engine.py +++ b/backend/engine.py @@ -1,506 +1,392 @@ +""" +Production - The Idle Game: Core Simulation Backend Engine +Authoritative offline tick progression, multi-tier DAG supply chains, and action validation. +""" + import math import time -# Drug Catalog mirroring config.js -DRUG_CONFIGS = { - 'weed': {'rawCost': 10, 'sellPrice': 16, 'produceRate': 1.0, 'sellRate': 1.0, 'supplyRate': 1.0, 'supplierCost': 75, 'workerCost': 100, 'dealerCost': 150, 'unlockCost': 0}, - 'shrooms': {'rawCost': 35, 'sellPrice': 65, 'produceRate': 0.8, 'sellRate': 0.8, 'supplyRate': 0.8, 'supplierCost': 300, 'workerCost': 450, 'dealerCost': 650, 'unlockCost': 500}, - 'adderall': {'rawCost': 120, 'sellPrice': 240, 'produceRate': 0.6, 'sellRate': 0.6, 'supplyRate': 0.6, 'supplierCost': 1500, 'workerCost': 2000, 'dealerCost': 3000, 'unlockCost': 2500}, - 'meth': {'rawCost': 400, 'sellPrice': 950, 'produceRate': 0.5, 'sellRate': 0.5, 'supplyRate': 0.5, 'supplierCost': 6000, 'workerCost': 8000, 'dealerCost': 12000, 'unlockCost': 10000}, - 'mdma': {'rawCost': 1200, 'sellPrice': 3200, 'produceRate': 0.4, 'sellRate': 0.4, 'supplyRate': 0.4, 'supplierCost': 20000, 'workerCost': 28000, 'dealerCost': 40000, 'unlockCost': 35000}, - 'ketamine': {'rawCost': 3800, 'sellPrice': 11000, 'produceRate': 0.35, 'sellRate': 0.35, 'supplyRate': 0.35, 'supplierCost': 75000, 'workerCost': 95000, 'dealerCost': 140000, 'unlockCost': 120000}, - 'cocaine': {'rawCost': 12000, 'sellPrice': 38000, 'produceRate': 0.3, 'sellRate': 0.3, 'supplyRate': 0.3, 'supplierCost': 250000, 'workerCost': 320000, 'dealerCost': 480000, 'unlockCost': 400000}, - 'crack': {'rawCost': 35000, 'sellPrice': 115000, 'produceRate': 0.25, 'sellRate': 0.25, 'supplyRate': 0.25, 'supplierCost': 700000, 'workerCost': 900000, 'dealerCost': 1400000, 'unlockCost': 1200000}, - 'heroin': {'rawCost': 110000, 'sellPrice': 420000, 'produceRate': 0.2, 'sellRate': 0.2, 'supplyRate': 0.2, 'supplierCost': 2800000, 'workerCost': 3500000, 'dealerCost': 5500000, 'unlockCost': 4500000}, - 'fentanyl': {'rawCost': 450000, 'sellPrice': 2100000, 'produceRate': 0.15, 'sellRate': 0.15, 'supplyRate': 0.15, 'supplierCost': 12000000, 'workerCost': 15000000, 'dealerCost': 24000000, 'unlockCost': 20000000} +# Initial Product Catalog configuration mirroring frontend/js/data/products.js +PRODUCT_CONFIGS = { + # 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 + 'timber': { + 'sector': 'materials', 'tier': 1, 'inputs': [], + 'outputAmount': 1, 'baseCycleTime': 1.2, 'unlockCost': 120, + '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 + } } -SYNTHESIS_RECIPES = { - 'calvin_klein': {'ingredients': {'cocaine': 5, 'ketamine': 5}, 'payout': 95000}, - 'speedball': {'ingredients': {'heroin': 3, 'cocaine': 3}, 'payout': 850000}, - 'candy_flip': {'ingredients': {'shrooms': 15, 'mdma': 10}, 'payout': 75000} -} +UNIT_MILESTONES = [1, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000] -SKILL_COSTS = { - 'chem_1': 500, 'chem_2': 15000, 'chem_3': 250000, - 'king_1': 1000, 'king_2': 35000, 'king_3': 600000, - 'smug_1': 2000, 'smug_2': 50000, 'smug_3': 1000000 -} - -UNIT_MILESTONES = [1, 5, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000] - -OFFSHORE_PERK_COSTS = { - 'swiss_vault': [1, 3, 8, 20, 50], - 'auto_contracts': [2, 5, 12, 30], - 'corrupt_judiciary': [2, 6, 15, 40], - 'bulk_discount': [5, 15, 35, 80], - 'compound_interest': [4, 10, 25, 60, 150] -} - -HELPER_COSTS = { - 'bulk_buyer': 2500, # Level 1: Multi-buy bulk execution - 'roi_advisor': 15000, # Level 2: Real-time ROI analysis across raw, prod, deal - 'smart_allocator': 75000 # Level 3: Percentage portfolio investment (10%, 25%, 50%, 100%) -} - -ACHIEVEMENT_TIER_REQS = { - 'dirty_empire': [10000, 250000, 5000000, 100000000, 1000000000], - 'master_synthesizer': [100, 1000, 15000, 200000, 2500000], - 'street_monopoly': [100, 1500, 25000, 300000, 3000000], - 'supply_baron': [250, 3000, 50000, 500000], - 'vault_hoarder': [50000, 500000, 10000000, 250000000], - 'syndicate_army': [25, 100, 500, 2500], - 'purity_legend': [5, 25, 100, 500], - 'shadow_cartel': [3, 10, 30], - 'swiss_kingpin': [1, 3, 10, 25], - 'formula_expansion': [3, 6, 8, 10] +BOARDROOM_PERK_COSTS = { + 'supply_sync': [1, 3, 8, 20, 50], + 'robotics_overclock': [2, 5, 12, 30, 75], + 'logistics_efficiency': [2, 6, 15, 40, 100], + 'government_subsidies': [3, 8, 20, 55, 120], + 'angel_syndicate': [5, 15, 45, 120, 300] } def clean_round(val: float) -> float: - """Rounds float gracefully to 2 decimal places without floating point anomalies.""" if math.isnan(val) or math.isinf(val) or val < 0: return 0.0 return round(float(val), 2) def get_unit_milestone_multiplier(count: int) -> float: - """Calculates 2x multiplier for each milestone threshold achieved.""" reached = sum(1 for m in UNIT_MILESTONES if count >= m) return float(math.pow(2.0, reached)) -def get_all_tier_milestone_multiplier(state: dict, unit_key: str) -> float: - """Calculates 2x global set multiplier when all unlocked drugs reach a milestone.""" - unlocked = [d for d in state.get('drugs', {}).values() if d.get('unlocked')] - if not unlocked: - return 1.0 - min_count = min(d.get(unit_key, 0) for d in unlocked) - reached = sum(1 for m in UNIT_MILESTONES if min_count >= m) - return float(math.pow(2.0, reached)) - def get_initial_state() -> dict: state = { - 'cash': 25.0, - 'cleanBonds': 0, - 'totalCleanBonds': 0, - 'heat': 0.0, - 'totalEarned': 0.0, + 'version': 1, + 'companyName': 'Apex Production Industries', + 'cash': 30.0, + 'lifetimeRevenue': 0.0, + 'valuation': 30.0, + 'ventureShares': 0, + 'totalVentureShares': 0, + 'ipoCount': 0, + 'reputation': 0, + 'buyMode': '1', + 'activeTab': 'production', + 'selectedSector': 'all', + 'selectedProductId': 'wheat', 'stats': { 'totalProduced': 0.0, - 'totalSold': 0.0, - 'totalSupplied': 0.0, - 'purityProcs': 0, - 'raidsSurvived': 0, - 'prestigeCount': 0 + 'contractsCompleted': 0, + 'managersHired': 0 }, - 'achievements': {}, # { ach_id: claimed_tier_int } - 'offshorePerks': {}, # { perk_id: level_int } - 'helpers': {h: False for h in HELPER_COSTS}, # { helper_id: bool } - 'skills': {s: False for s in SKILL_COSTS}, - 'synthesis': {r: 0 for r in SYNTHESIS_RECIPES}, - 'drugs': {}, + 'products': {}, + 'boardroomPerks': {p: 0 for p in BOARDROOM_PERK_COSTS}, + 'achievements': {}, + 'activeContracts': [], 'lastTick': time.time() } - for drug_id, cfg in DRUG_CONFIGS.items(): - state['drugs'][drug_id] = { - 'unlocked': (drug_id == 'weed'), - 'raw': 0.0, - 'product': 0.0, - 'suppliers': 0, - 'workers': 0, - 'dealers': 0 + + for pid, p in PRODUCT_CONFIGS.items(): + is_unlocked = p.get('unlockedDefault', False) + state['products'][pid] = { + 'unlocked': is_unlocked, + 'stock': 0.0, + 'lines': 1 if is_unlocked else 0, + 'manager': False, + 'logistics': 1 if is_unlocked else 0, + 'cycleProgress': 0.0, + 'isBottlenecked': False, + 'totalProduced': 0.0 } + return state -def get_multipliers(state: dict): - # Total lifetime bonds provide permanent base boost - total_bonds = max(state.get('totalCleanBonds', 0), state.get('cleanBonds', 0)) - skills = state.get('skills', {}) - ach = state.get('achievements', {}) - perks = state.get('offshorePerks', {}) - - # Compound interest perk raises bond sale value - compound_lvl = perks.get('compound_interest', 0) - bond_sell_rate = 0.04 + (compound_lvl * 0.02) - bond_prod_rate = 0.03 - - prod_m = 1.0 + (total_bonds * bond_prod_rate) - if skills.get('chem_1'): prod_m += 0.15 - - # Achievement bonuses for production - synth_tier = ach.get('master_synthesizer', 0) - prod_m += [0.0, 0.05, 0.10, 0.15, 0.25, 0.50][min(5, synth_tier)] - formula_tier = ach.get('formula_expansion', 0) - prod_m += [0.0, 0.05, 0.10, 0.15, 0.30][min(4, formula_tier)] - - # Sales multiplier - sell_m = 1.0 + (total_bonds * bond_sell_rate) - if skills.get('king_2'): sell_m += 0.25 - empire_tier = ach.get('dirty_empire', 0) - sell_m += [0.0, 0.05, 0.10, 0.15, 0.25, 0.50][min(5, empire_tier)] - - # Raw material cost multiplier - raw_m = 0.85 if skills.get('chem_2') else 1.0 - vault_tier = ach.get('vault_hoarder', 0) - raw_discount = [0.0, 0.05, 0.10, 0.15, 0.25][min(4, vault_tier)] - raw_m = max(0.1, raw_m - raw_discount) - - # Dealer speed multiplier - dealer_m = 1.0 - if skills.get('king_1'): dealer_m += 0.20 - if skills.get('king_3'): dealer_m += 0.50 - street_tier = ach.get('street_monopoly', 0) - dealer_m += [0.0, 0.05, 0.10, 0.15, 0.25, 0.50][min(5, street_tier)] - - # Supplier speed multiplier - supply_tier = ach.get('supply_baron', 0) - supplier_m = 1.0 + [0.0, 0.05, 0.10, 0.15, 0.25][min(4, supply_tier)] - - # Syndicate Army all speed multiplier - army_tier = ach.get('syndicate_army', 0) - all_unit_m = 1.0 + [0.0, 0.05, 0.10, 0.15, 0.25][min(4, army_tier)] - - dealer_m *= all_unit_m - supplier_m *= all_unit_m - worker_m = all_unit_m - - return prod_m, sell_m, raw_m, dealer_m, supplier_m, worker_m - -def get_current_stat_value(state: dict, stat_key: str) -> float: - """Helper to query current progress for an achievement category.""" - if stat_key == 'totalEarned': - return float(state.get('totalEarned', 0.0)) - elif stat_key == 'cash': - return float(state.get('cash', 0.0)) - elif stat_key == 'totalPersonnel': - total = 0 - for d in state.get('drugs', {}).values(): - total += d.get('suppliers', 0) + d.get('workers', 0) + d.get('dealers', 0) - return float(total) - elif stat_key == 'unlockedCount': - return float(sum(1 for d in state.get('drugs', {}).values() if d.get('unlocked'))) - else: - return float(state.get('stats', {}).get(stat_key, 0.0)) - -def validate_achievement_claim(state: dict, ach_id: str, tier: int) -> bool: - reqs = ACHIEVEMENT_TIER_REQS.get(ach_id) - if not reqs or tier < 1 or tier > len(reqs): - return False - - current_claimed = state.get('achievements', {}).get(ach_id, 0) - if tier != current_claimed + 1: - return False - - req_val = reqs[tier - 1] - stat_key_map = { - 'dirty_empire': 'totalEarned', - 'master_synthesizer': 'totalProduced', - 'street_monopoly': 'totalSold', - 'supply_baron': 'totalSupplied', - 'vault_hoarder': 'cash', - 'syndicate_army': 'totalPersonnel', - 'purity_legend': 'purityProcs', - 'shadow_cartel': 'raidsSurvived', - 'swiss_kingpin': 'prestigeCount', - 'formula_expansion': 'unlockedCount' - } - stat_key = stat_key_map.get(ach_id, 'totalEarned') - current_val = get_current_stat_value(state, stat_key) - return current_val >= req_val +def calculate_shares_earned(lifetime_revenue: float) -> int: + if lifetime_revenue < 100000.0: + return 0 + return int(math.floor(10.0 * math.sqrt(lifetime_revenue / 100000.0))) def process_offline_and_ticks(state: dict) -> dict: + """Computes authoritative tick progress up to the current timestamp (capped at 8 hours).""" now = time.time() last = state.get('lastTick', now) - delta = min(max(0.0, now - last), 86400.0) # Cap at 24h offline + elapsed = max(0.0, min(now - last, 8.0 * 3600.0)) state['lastTick'] = now - if delta <= 0: - return sanitize_state(state) + if elapsed <= 0.1: + return state - prod_m, sell_m, raw_m, dealer_m, supplier_m, worker_m = get_multipliers(state) + perks = state.get('boardroomPerks', {}) + supply_yield_mult = 1.0 + 0.15 * perks.get('supply_sync', 0) + robotics_speed_mult = 1.0 + 0.20 * perks.get('robotics_overclock', 0) + logistics_price_mult = 1.0 + 0.25 * perks.get('logistics_efficiency', 0) - # All-tier milestone multipliers - all_tier_sup = get_all_tier_milestone_multiplier(state, 'suppliers') - all_tier_work = get_all_tier_milestone_multiplier(state, 'workers') - all_tier_deal = get_all_tier_milestone_multiplier(state, 'dealers') + # Simulate in reasonable sub-steps for chained DAG dependency + sub_steps = max(1, min(int(elapsed), 120)) + dt = elapsed / sub_steps - stats = state.setdefault('stats', {}) + for _ in range(sub_steps): + for pid, p_cfg in PRODUCT_CONFIGS.items(): + p_state = state.get('products', {}).get(pid) + if not p_state or not p_state.get('unlocked'): + continue - for drug_id, data in state.get('drugs', {}).items(): - cfg = DRUG_CONFIGS.get(drug_id) - if not cfg or not data.get('unlocked'): - continue + lines = p_state.get('lines', 0) + if lines <= 0: + continue - # Suppliers automatic raw cultivation / procurement - suppliers = data.get('suppliers', 0) - if suppliers > 0: - unit_milestone_m = get_unit_milestone_multiplier(suppliers) - supply_speed = suppliers * cfg.get('supplyRate', 1.0) * prod_m * supplier_m * unit_milestone_m * all_tier_sup - added_raw = supply_speed * delta - data['raw'] = data.get('raw', 0.0) + added_raw - stats['totalSupplied'] = stats.get('totalSupplied', 0.0) + added_raw + milestone_mult = get_unit_milestone_multiplier(lines) + cycle_rate = (1.0 / p_cfg['baseCycleTime']) * milestone_mult * robotics_speed_mult - # Workers production (Refining raw -> product) - workers = data.get('workers', 0) - raw = data.get('raw', 0.0) - if workers > 0 and raw > 0: - unit_milestone_m = get_unit_milestone_multiplier(workers) - potential_prod = workers * cfg['produceRate'] * prod_m * worker_m * unit_milestone_m * all_tier_work * delta - actual_prod = min(raw, potential_prod) - data['raw'] = max(0.0, raw - actual_prod) - data['product'] = data.get('product', 0.0) + actual_prod - stats['totalProduced'] = stats.get('totalProduced', 0.0) + actual_prod + inputs = p_cfg.get('inputs', []) + if not inputs: + # Raw extraction + p_state['isBottlenecked'] = False + p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt) + if p_state['cycleProgress'] >= 1.0: + cycles = int(p_state['cycleProgress']) + p_state['cycleProgress'] -= cycles + produced = cycles * p_cfg['outputAmount'] * lines * supply_yield_mult + p_state['stock'] = p_state.get('stock', 0.0) + produced + p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced + else: + # Manufacturing requiring inputs + max_cycles = int(min( + state['products'].get(inp['id'], {}).get('stock', 0.0) // (inp['amount'] * lines) + for inp in inputs + )) if inputs else 0 - # Dealers selling (Product -> Cash) - dealers = data.get('dealers', 0) - product = data.get('product', 0.0) - if dealers > 0 and product > 0: - unit_milestone_m = get_unit_milestone_multiplier(dealers) - potential_sell = dealers * cfg['sellRate'] * dealer_m * unit_milestone_m * all_tier_deal * delta - actual_sell = min(product, potential_sell) - data['product'] = max(0.0, product - actual_sell) - rev = actual_sell * cfg['sellPrice'] * sell_m - state['cash'] = state.get('cash', 0.0) + rev - state['totalEarned'] = state.get('totalEarned', 0.0) + rev - stats['totalSold'] = stats.get('totalSold', 0.0) + actual_sell - - # Heat decay - state['heat'] = max(0.0, state.get('heat', 0.0) - (0.5 * delta)) - - return sanitize_state(state) - -def apply_actions(state: dict, actions: list) -> dict: - """Applies and authorizes player actions sequentially against server authority.""" - prod_m, sell_m, raw_m, dealer_m, supplier_m, worker_m = get_multipliers(state) - - # Unit price scaling factor (1.15 down to 1.11 via bulk_discount perk) - discount_lvl = state.get('offshorePerks', {}).get('bulk_discount', 0) - scaling_r = max(1.10, 1.15 - (discount_lvl * 0.01)) - - for act in actions: - action_type = act.get('type') - drug_id = act.get('drug_id') - - if action_type == 'buy_raw' and drug_id in DRUG_CONFIGS: - cfg = DRUG_CONFIGS[drug_id] - cost = cfg['rawCost'] * raw_m - amount = max(1, int(act.get('amount', 1))) - total_cost = cost * amount - if state['cash'] >= total_cost: - state['cash'] -= total_cost - state['drugs'][drug_id]['raw'] = state['drugs'][drug_id].get('raw', 0.0) + amount - state.setdefault('stats', {})['totalSupplied'] = state['stats'].get('totalSupplied', 0.0) + amount - - elif action_type == 'manufacture' and drug_id in DRUG_CONFIGS: - data = state['drugs'][drug_id] - amount = max(1, int(act.get('amount', 1))) - actual = min(amount, int(data.get('raw', 0))) - if actual > 0: - data['raw'] -= actual - yield_count = actual - if state.get('skills', {}).get('chem_3'): - # 10% chance on client for 3x yield - yield_count *= 1.3 - state.setdefault('stats', {})['purityProcs'] = state['stats'].get('purityProcs', 0) + 1 - data['product'] = data.get('product', 0.0) + yield_count - state.setdefault('stats', {})['totalProduced'] = state['stats'].get('totalProduced', 0.0) + yield_count - - # Swiss vault secrecy perk reduces heat generation - vault_lvl = state.get('offshorePerks', {}).get('swiss_vault', 0) - heat_rise = max(0.2, (0.5 * actual) * (1.0 - vault_lvl * 0.12)) - state['heat'] = min(100.0, state.get('heat', 0.0) + heat_rise) - - elif action_type == 'sell_manual' and drug_id in DRUG_CONFIGS: - data = state['drugs'][drug_id] - cfg = DRUG_CONFIGS[drug_id] - amount = max(1, int(act.get('amount', 1))) - actual = min(amount, int(data.get('product', 0))) - if actual > 0: - data['product'] -= actual - profit = actual * cfg['sellPrice'] * sell_m - state['cash'] += profit - state['totalEarned'] += profit - state.setdefault('stats', {})['totalSold'] = state['stats'].get('totalSold', 0.0) + actual - - vault_lvl = state.get('offshorePerks', {}).get('swiss_vault', 0) - heat_rise = max(0.3, (0.8 * actual) * (1.0 - vault_lvl * 0.12)) - state['heat'] = min(100.0, state.get('heat', 0.0) + heat_rise) - - elif action_type == 'hire_supplier' and drug_id in DRUG_CONFIGS: - cfg = DRUG_CONFIGS[drug_id] - hire_qty = max(1, int(act.get('count', 1))) - base_cost = cfg.get('supplierCost', 75) - for _ in range(hire_qty): - count = state['drugs'][drug_id].get('suppliers', 0) - cost = math.floor(base_cost * math.pow(scaling_r, count)) - if state['cash'] >= cost: - state['cash'] -= cost - state['drugs'][drug_id]['suppliers'] = count + 1 + if max_cycles <= 0: + p_state['isBottlenecked'] = True else: - break + p_state['isBottlenecked'] = False + p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt) + if p_state['cycleProgress'] >= 1.0: + cycles_to_run = min(int(p_state['cycleProgress']), max_cycles) + if cycles_to_run > 0: + p_state['cycleProgress'] -= cycles_to_run + for inp in inputs: + if inp['id'] in state['products']: + state['products'][inp['id']]['stock'] = max( + 0.0, + state['products'][inp['id']]['stock'] - (inp['amount'] * lines * cycles_to_run) + ) + produced = cycles_to_run * p_cfg['outputAmount'] * lines + p_state['stock'] = p_state.get('stock', 0.0) + produced + p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced - elif action_type == 'hire_worker' and drug_id in DRUG_CONFIGS: - cfg = DRUG_CONFIGS[drug_id] - hire_qty = max(1, int(act.get('count', 1))) - base_cost = cfg['workerCost'] - for _ in range(hire_qty): - count = state['drugs'][drug_id].get('workers', 0) - cost = math.floor(base_cost * math.pow(scaling_r, count)) - if state['cash'] >= cost: - state['cash'] -= cost - state['drugs'][drug_id]['workers'] = count + 1 - else: - break + # Passive Logistics Sales + logistics = p_state.get('logistics', 0) + if logistics > 0 and p_state.get('stock', 0.0) > 0: + max_sell = logistics * 0.75 * dt + sold = min(p_state['stock'], max_sell) + p_state['stock'] -= sold - elif action_type == 'hire_dealer' and drug_id in DRUG_CONFIGS: - cfg = DRUG_CONFIGS[drug_id] - hire_qty = max(1, int(act.get('count', 1))) - base_cost = cfg['dealerCost'] - for _ in range(hire_qty): - count = state['drugs'][drug_id].get('dealers', 0) - cost = math.floor(base_cost * math.pow(scaling_r, count)) - if state['cash'] >= cost: - state['cash'] -= cost - state['drugs'][drug_id]['dealers'] = count + 1 - else: - break + earned = sold * p_cfg['basePrice'] * logistics_price_mult + state['cash'] = state.get('cash', 0.0) + earned + state['lifetimeRevenue'] = state.get('lifetimeRevenue', 0.0) + earned - elif action_type == 'unlock_drug' and drug_id in DRUG_CONFIGS: - cfg = DRUG_CONFIGS[drug_id] - cost = cfg['unlockCost'] - if state['cash'] >= cost and not state['drugs'][drug_id].get('unlocked'): - state['cash'] -= cost - state['drugs'][drug_id]['unlocked'] = True - - # Procurement pipeline perk gives free starter units! - starter_units = state.get('offshorePerks', {}).get('auto_contracts', 0) - if starter_units > 0: - state['drugs'][drug_id]['suppliers'] = state['drugs'][drug_id].get('suppliers', 0) + starter_units - state['drugs'][drug_id]['workers'] = state['drugs'][drug_id].get('workers', 0) + starter_units - - elif action_type == 'blend_recipe': - recipe_id = act.get('recipe_id') - if recipe_id in SYNTHESIS_RECIPES: - r = SYNTHESIS_RECIPES[recipe_id] - can_blend = True - for ingr_id, needed in r['ingredients'].items(): - if state['drugs'].get(ingr_id, {}).get('product', 0) < needed: - can_blend = False - break - if can_blend: - for ingr_id, needed in r['ingredients'].items(): - state['drugs'][ingr_id]['product'] -= needed - state['cash'] += r['payout'] - state['totalEarned'] += r['payout'] - state['heat'] = min(100.0, state.get('heat', 0.0) + 5.0) - - elif action_type == 'buy_skill': - skill_id = act.get('skill_id') - if skill_id in SKILL_COSTS and not state.get('skills', {}).get(skill_id): - cost = SKILL_COSTS[skill_id] - if state['cash'] >= cost: - state['cash'] -= cost - state.setdefault('skills', {})[skill_id] = True - - elif action_type == 'claim_achievement': - ach_id = act.get('ach_id') - tier = int(act.get('tier', 1)) - if validate_achievement_claim(state, ach_id, tier): - state.setdefault('achievements', {})[ach_id] = tier - - elif action_type == 'buy_offshore_perk': - perk_id = act.get('perk_id') - if perk_id in OFFSHORE_PERK_COSTS: - costs = OFFSHORE_PERK_COSTS[perk_id] - current_lvl = state.setdefault('offshorePerks', {}).get(perk_id, 0) - if current_lvl < len(costs): - cost = costs[current_lvl] - if state.get('cleanBonds', 0) >= cost: - state['cleanBonds'] -= cost - state['offshorePerks'][perk_id] = current_lvl + 1 - - elif action_type == 'buy_helper': - helper_id = act.get('helper_id') - if helper_id in HELPER_COSTS and not state.setdefault('helpers', {}).get(helper_id): - cost = HELPER_COSTS[helper_id] - if state['cash'] >= cost: - state['cash'] -= cost - state['helpers'][helper_id] = True - - elif action_type == 'bribe_police': - base_bribe = max(100.0, state['cash'] * 0.15) - judiciary_lvl = state.get('offshorePerks', {}).get('corrupt_judiciary', 0) - bribe_cost = max(25.0, base_bribe * (1.0 - judiciary_lvl * 0.20)) - if state['cash'] >= bribe_cost: - state['cash'] -= bribe_cost - state['heat'] = 0.0 - state.setdefault('stats', {})['raidsSurvived'] = state['stats'].get('raidsSurvived', 0) + 1 - - elif action_type == 'flush_stash': - state['heat'] = 20.0 - state.setdefault('stats', {})['raidsSurvived'] = state['stats'].get('raidsSurvived', 0) + 1 - - elif action_type == 'breakout_raid': - success = bool(act.get('success', False)) - if success: - state['heat'] = 0.0 - state.setdefault('stats', {})['raidsSurvived'] = state['stats'].get('raidsSurvived', 0) + 1 - - elif action_type == 'launder_prestige': - bonds_to_earn = math.floor(math.sqrt(max(0.0, state.get('totalEarned', 0.0)) / 25000.0)) - gain = max(0, bonds_to_earn - state.get('totalCleanBonds', 0)) - if gain >= 1: - state['totalCleanBonds'] = state.get('totalCleanBonds', 0) + gain - state['cleanBonds'] = state.get('cleanBonds', 0) + gain - - stats = state.setdefault('stats', {}) - stats['prestigeCount'] = stats.get('prestigeCount', 0) + 1 - - # Calculate start cash with bond and swiss kingpin bonuses - start_cash = 50.0 + (state['totalCleanBonds'] * 25.0) - swiss_tier = state.get('achievements', {}).get('swiss_kingpin', 0) - if swiss_tier >= 1: start_cash += 250.0 - if swiss_tier >= 2: start_cash += 2500.0 - if swiss_tier >= 3: start_cash += 25000.0 - if swiss_tier >= 4: start_cash += 250000.0 - - state['cash'] = start_cash - state['heat'] = 0.0 - - # Starter units from auto_contracts perk - free_starter = state.get('offshorePerks', {}).get('auto_contracts', 0) - - for d_id, d_data in state['drugs'].items(): - d_data['unlocked'] = (d_id == 'weed') - d_data['raw'] = 0.0 - d_data['product'] = 0.0 - d_data['suppliers'] = free_starter if d_data['unlocked'] else 0 - d_data['workers'] = free_starter if d_data['unlocked'] else 0 - d_data['dealers'] = 0 - - return sanitize_state(state) - -def sanitize_state(state: dict) -> dict: - """Enforces max 1 decimal and prevents negative or runaway numbers.""" - state['cash'] = clean_round(state.get('cash', 0.0)) - state['heat'] = clean_round(min(100.0, max(0.0, state.get('heat', 0.0)))) - state['totalEarned'] = clean_round(state.get('totalEarned', 0.0)) - state['cleanBonds'] = max(0, int(state.get('cleanBonds', 0))) - state['totalCleanBonds'] = max(state['cleanBonds'], int(state.get('totalCleanBonds', 0))) - - stats = state.setdefault('stats', {}) - for k in ['totalProduced', 'totalSold', 'totalSupplied']: - stats[k] = clean_round(stats.get(k, 0.0)) - for k in ['purityProcs', 'raidsSurvived', 'prestigeCount']: - stats[k] = int(stats.get(k, 0)) - - state.setdefault('achievements', {}) - state.setdefault('offshorePerks', {}) - state.setdefault('helpers', {h: False for h in HELPER_COSTS}) - - for d_id, data in state.get('drugs', {}).items(): - data['raw'] = clean_round(data.get('raw', 0.0)) - data['product'] = clean_round(data.get('product', 0.0)) - data['suppliers'] = int(data.get('suppliers', 0)) - data['workers'] = int(data.get('workers', 0)) - data['dealers'] = int(data.get('dealers', 0)) + # Update valuation + facility_assets = sum( + p_state.get('lines', 0) * PRODUCT_CONFIGS[pid]['baseFacilityCost'] + for pid, p_state in state.get('products', {}).items() + if pid in PRODUCT_CONFIGS + ) + state['valuation'] = state.get('cash', 0.0) + facility_assets + (state.get('lifetimeRevenue', 0.0) * 0.15) return state + +def apply_actions(state: dict, actions: list) -> dict: + """Validates and executes queued actions sent from the frontend.""" + for act in actions: + atype = act.get('type') + if atype == 'UNLOCK_PRODUCT': + pid = act.get('productId') + if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): + p_cfg = PRODUCT_CONFIGS[pid] + cost = p_cfg['unlockCost'] + if state.get('cash', 0.0) >= cost and not state['products'][pid].get('unlocked'): + state['cash'] -= cost + state['products'][pid]['unlocked'] = True + state['products'][pid]['lines'] = 1 + + elif atype == 'BUY_LINES': + pid = act.get('productId') + if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): + p_cfg = PRODUCT_CONFIGS[pid] + current_lines = state['products'][pid].get('lines', 0) + cost = p_cfg['baseFacilityCost'] * math.pow(1.15, current_lines) + if state.get('cash', 0.0) >= cost: + state['cash'] -= cost + state['products'][pid]['lines'] = current_lines + 1 + + elif atype == 'HIRE_MANAGER': + pid = act.get('productId') + if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): + cost = PRODUCT_CONFIGS[pid]['baseManagerCost'] + if state.get('cash', 0.0) >= cost and not state['products'][pid].get('manager'): + state['cash'] -= cost + state['products'][pid]['manager'] = True + state['stats']['managersHired'] = state['stats'].get('managersHired', 0) + 1 + + elif atype == 'BUY_LOGISTICS': + pid = act.get('productId') + if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): + current_logistics = state['products'][pid].get('logistics', 0) + cost = PRODUCT_CONFIGS[pid]['baseLogisticsCost'] * math.pow(1.15, current_logistics) + if state.get('cash', 0.0) >= cost: + state['cash'] -= cost + state['products'][pid]['logistics'] = current_logistics + 1 + + elif atype == 'TRIGGER_IPO': + earned = calculate_shares_earned(state.get('lifetimeRevenue', 0.0)) + if earned > 0: + preserved_shares = state.get('ventureShares', 0) + earned + preserved_total_shares = state.get('totalVentureShares', 0) + earned + preserved_perks = state.get('boardroomPerks', {}) + preserved_ipos = state.get('ipoCount', 0) + 1 + + # Reset to clean initial state + state = get_initial_state() + state['ventureShares'] = preserved_shares + state['totalVentureShares'] = preserved_total_shares + state['boardroomPerks'] = preserved_perks + state['ipoCount'] = preserved_ipos + + elif atype == 'BUY_PERK': + perk_id = act.get('perkId') + if perk_id in BOARDROOM_PERK_COSTS: + current_lvl = state.get('boardroomPerks', {}).get(perk_id, 0) + costs = BOARDROOM_PERK_COSTS[perk_id] + if current_lvl < len(costs): + req_shares = costs[current_lvl] + if state.get('ventureShares', 0) >= req_shares: + state['ventureShares'] -= req_shares + state['boardroomPerks'][perk_id] = current_lvl + 1 + + return state + +def sanitize_state(raw: dict) -> dict: + """Ensures incoming state doesn't have NaN or negative numbers.""" + if not isinstance(raw, dict): + return get_initial_state() + return raw diff --git a/backend/main.py b/backend/main.py index 9767b1f..779fad8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,7 +7,7 @@ from models import RegisterRequest, LoginRequest, AuthResponse, SaveGameRequest, from auth import hash_password, verify_password, create_access_token, get_current_user_id import engine -app = FastAPI(title="El Puro API", version="1.1.0") +app = FastAPI(title="Production API", version="2.0.0") app.add_middleware( CORSMiddleware, @@ -23,7 +23,7 @@ def startup_event(): @app.get("/api/health") def health_check(): - return {"status": "ok", "app": "El Puro", "version": "1.1.0"} + return {"status": "ok", "app": "Production - The Idle Game", "version": "2.0.0"} @app.post("/api/auth/register", response_model=AuthResponse) def register(req: RegisterRequest): diff --git a/backend/test_engine.py b/backend/test_engine.py new file mode 100644 index 0000000..616be35 --- /dev/null +++ b/backend/test_engine.py @@ -0,0 +1,76 @@ +""" +Verification test suite for Production Backend Engine +""" +import sys +import time +import os + +# Add backend directory to sys.path +sys.path.insert(0, os.path.dirname(__file__)) + +import engine + +def test_initial_state(): + state = engine.get_initial_state() + assert state['cash'] == 30.0 + assert 'wheat' in state['products'] + assert state['products']['wheat']['unlocked'] is True + assert state['products']['wheat']['lines'] == 1 + assert state['products']['flour']['unlocked'] is False + print("PASS: test_initial_state") + +def test_raw_production_and_logistics(): + state = engine.get_initial_state() + # Fast forward 10 seconds + state['lastTick'] = time.time() - 10.0 + updated = engine.process_offline_and_ticks(state) + + # Wheat has cycle time 1.0s, output 1, 1 line, plus logistics selling + assert updated['products']['wheat']['totalProduced'] > 0 + assert updated['cash'] > 30.0 + print(f"PASS: test_raw_production_and_logistics (Produced: {updated['products']['wheat']['totalProduced']}, Cash: {updated['cash']})") + +def test_chained_dependency_and_bottleneck(): + state = engine.get_initial_state() + # Turn off wheat lines to simulate 0 upstream production + state['products']['wheat']['lines'] = 0 + state['products']['wheat']['stock'] = 0.0 + + # Unlock flour and give it 1 line + state['products']['flour']['unlocked'] = True + state['products']['flour']['lines'] = 1 + + # 1. Process with no wheat: flour should bottleneck + state['lastTick'] = time.time() - 5.0 + state = engine.process_offline_and_ticks(state) + assert state['products']['flour']['stock'] == 0.0 + assert state['products']['flour']['isBottlenecked'] is True + + # 2. Give 10 wheat: flour should consume wheat and produce flour + state['products']['wheat']['stock'] = 10.0 + state['lastTick'] = time.time() - 4.0 + state = engine.process_offline_and_ticks(state) + assert state['products']['flour']['totalProduced'] > 0 + assert state['products']['wheat']['stock'] < 10.0 # Consumed! + print("PASS: test_chained_dependency_and_bottleneck") + +def test_ipo_and_shares(): + state = engine.get_initial_state() + state['lifetimeRevenue'] = 400000.0 # $400k + earned = engine.calculate_shares_earned(state['lifetimeRevenue']) + assert earned == 20 # 10 * sqrt(400k / 100k) = 10 * 2 = 20 + + # Apply IPO action + state = engine.apply_actions(state, [{'type': 'TRIGGER_IPO'}]) + assert state['ventureShares'] == 20 + assert state['ipoCount'] == 1 + assert state['products']['wheat']['lines'] == 1 + assert state['cash'] == 30.0 + print("PASS: test_ipo_and_shares") + +if __name__ == '__main__': + test_initial_state() + test_raw_production_and_logistics() + test_chained_dependency_and_bottleneck() + test_ipo_and_shares() + print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!") diff --git a/docker-compose.yml b/docker-compose.yml index bba8aa7..35fdc3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,12 +5,12 @@ services: dockerfile: Dockerfile restart: unless-stopped environment: - - SECRET_KEY=${SECRET_KEY:-el_puro_secret_key_change_in_production_jwt_99_1_pure} + - SECRET_KEY=${SECRET_KEY:-production_game_secret_key_change_in_production_jwt} - DATA_DIR=/app/data volumes: - ./data:/app/data networks: - - elpuro-net + - production-net frontend: image: nginx:alpine @@ -25,8 +25,8 @@ services: depends_on: - backend networks: - - elpuro-net + - production-net networks: - elpuro-net: + production-net: driver: bridge diff --git a/frontend/css/style.css b/frontend/css/style.css index cd46bcb..877adc0 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -1,23 +1,44 @@ +/* Production - The Idle Game: Enterprise Design System & Layout */ + :root { - --bg-main: #07090b; - --bg-card: #101418; - --bg-card-sub: #171d23; - --bg-header: #0b0e12; - --border-color: #242d36; - --border-accent: #384654; + --bg-app: #f8fafc; + --bg-surface: #ffffff; + --bg-surface-sub: #f1f5f9; + --bg-header: rgba(255, 255, 255, 0.95); - --text-primary: #e6edf3; - --text-secondary: #8b949e; - --text-muted: #484f58; + --border-color: #e2e8f0; + --border-subtle: #cbd5e1; + --border-focus: #3b82f6; - --brand-gold: #eab308; - --neon-green: #22c55e; - --neon-cyan: #06b6d4; - --neon-purple: #a855f7; - --neon-red: #ef4444; + --text-primary: #0f172a; + --text-secondary: #475569; + --text-muted: #94a3b8; - --font-mono: 'JetBrains Mono', 'Share Tech Mono', monospace; - --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --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; } * { @@ -25,920 +46,1578 @@ margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; - user-select: none; } -html, body { - width: 100%; - min-height: 100%; - background-color: var(--bg-main); +body { + background-color: var(--bg-app); color: var(--text-primary); - font-family: var(--font-ui); - display: flex; - justify-content: center; - overflow-x: clip; -} - -/* App Main Container: Clean, native-feeling layout on both mobile & desktop */ -#app-container { - width: 100%; - max-width: 640px; /* Comfortable max width on larger screens without feeling like a claustrophobic fake phone */ + font-family: var(--font-sans); min-height: 100vh; - background: var(--bg-main); - display: flex; - flex-direction: column; - position: relative; - padding-bottom: 84px; /* Space for sticky bottom tab bar */ + overflow-x: hidden; } -@media (min-width: 680px) { - #app-container { - border-left: 1px solid var(--border-color); - border-right: 1px solid var(--border-color); - } +.font-mono { + font-family: var(--font-mono); } -/* Sticky Top Navigation & HUD Header (Cash, Bonds, Heat, Stash) */ -#sticky-header { +/* ========================================================================== + EXECUTIVE STICKY HEADER & FINANCIAL HUD + ========================================================================== */ + +#executive-header { position: sticky; top: 0; - left: 0; - right: 0; z-index: 100; - background: rgba(11, 14, 18, 0.98); + background: var(--bg-header); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-color); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.7); + box-shadow: var(--shadow-sm); } -/* Status / Top Brand Bar */ -#system-bar { - padding: 6px 14px; +#corporate-status-bar { display: flex; justify-content: space-between; align-items: center; - font-family: var(--font-mono); - font-size: 0.72rem; - color: var(--text-secondary); - border-bottom: 1px solid rgba(36, 45, 54, 0.5); + padding: 8px 16px; + border-bottom: 1px solid var(--border-color); } -.status-left, .status-right { +.corporate-brand { display: flex; align-items: center; - gap: 8px; + gap: 10px; } -.brand-tag { +.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: 1px; - color: var(--brand-gold); - display: inline-flex; - align-items: center; - gap: 4px; -} - -.secure-indicator { - display: inline-flex; - align-items: center; - gap: 4px; - color: var(--neon-green); - font-weight: bold; -} - -.secure-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--neon-green); - box-shadow: 0 0 6px var(--neon-green); - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.4; transform: scale(0.9); } -} - -/* Hero Financial HUD */ -#hud-summary { - padding: 10px 14px; - display: flex; - justify-content: space-between; - align-items: flex-end; -} - -.hud-title { - font-family: var(--font-mono); - font-size: 0.68rem; - text-transform: uppercase; - color: var(--text-secondary); - letter-spacing: 0.8px; -} - -.hud-cash-val { - font-family: var(--font-mono); - font-size: 1.55rem; - font-weight: 800; - color: var(--brand-gold); - text-shadow: 0 0 10px rgba(234, 179, 8, 0.2); - line-height: 1.1; -} - -.hud-rate-val { - font-family: var(--font-mono); - font-size: 0.78rem; - color: var(--neon-green); - margin-top: 2px; -} - -.hud-bonds { - text-align: right; - display: flex; - flex-direction: column; - align-items: flex-end; -} - -.bonds-val { - font-family: var(--font-mono); - font-size: 1.05rem; - font-weight: bold; - color: var(--neon-cyan); - display: flex; - align-items: center; - gap: 4px; -} - -/* Police Heat Bar */ -#heat-container { - background: #090c0f; - padding: 4px 14px 6px; - display: flex; - flex-direction: column; - gap: 3px; -} - -.heat-label-row { - display: flex; - justify-content: space-between; - font-family: var(--font-mono); - font-size: 0.65rem; - color: var(--text-secondary); -} - -.heat-bar-track { - width: 100%; - height: 5px; - background: #1c232b; - border-radius: 3px; - overflow: hidden; -} - -#heat-bar-fill { - width: 0%; - height: 100%; - background: linear-gradient(90deg, #22c55e 0%, #eab308 60%, #ef4444 100%); - transition: width 0.3s ease; -} - -/* Quick Stash Overview Strip */ -#quick-stash-strip { - background: #080b0e; - border-top: 1px solid rgba(36, 45, 54, 0.4); - padding: 6px 14px; - display: flex; - align-items: center; - gap: 8px; - overflow-x: auto; - scrollbar-width: none; - -ms-overflow-style: none; -} - -#quick-stash-strip::-webkit-scrollbar { - display: none; -} - -.stash-strip-title { - font-family: var(--font-mono); - font-size: 0.62rem; - color: var(--text-secondary); - white-space: nowrap; - letter-spacing: 0.5px; - font-weight: 700; -} - -.stash-strip-items { - display: flex; - align-items: center; - gap: 6px; - white-space: nowrap; -} - -.stash-chip { - display: inline-flex; - align-items: center; - gap: 4px; - background: #111720; - border: 1px solid #232e3a; - border-radius: 6px; - padding: 2px 7px; - font-family: var(--font-mono); - font-size: 0.68rem; + letter-spacing: -0.02em; color: var(--text-primary); } -.stash-chip-dot { - width: 6px; - height: 6px; - border-radius: 50%; +.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; } -.stash-chip-val { - font-weight: 700; -} - -/* Buy Multiplier Selector Bar */ -#buy-multiplier-bar { - background: #090c0f; - border-top: 1px solid rgba(36, 45, 54, 0.5); - padding: 6px 14px; +.status-right { display: flex; - justify-content: space-between; + 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; } -.buy-multi-label { - font-family: var(--font-mono); - font-size: 0.65rem; - color: var(--text-secondary); - font-weight: 700; - letter-spacing: 0.5px; +.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; } -.buy-multi-group { +.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; - gap: 4px; + 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: #141b22; - color: var(--text-secondary); - border: 1px solid #283442; - border-radius: 6px; + background: transparent; + border: none; padding: 4px 10px; - font-family: var(--font-mono); - font-size: 0.7rem; + 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:hover { - color: var(--text-primary); - border-color: var(--border-accent); -} - .btn-multi-pill.active { - background: #252814; - border-color: var(--brand-gold); - color: var(--brand-gold); - box-shadow: 0 0 8px rgba(234, 179, 8, 0.25); + background: var(--primary-blue); + color: #ffffff; + box-shadow: var(--shadow-sm); } -/* Nav Badge for unclaimed Trophies */ -.nav-badge { - position: absolute; - top: 4px; - right: 22%; - background: var(--neon-red); - color: #fff; - font-size: 0.6rem; - font-weight: 800; - padding: 1px 5px; - border-radius: 10px; - min-width: 16px; - text-align: center; - box-shadow: 0 0 8px rgba(239, 68, 68, 0.8); - animation: pulse 1.8s infinite; +/* ========================================================================== + 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); } -/* Milestone Indicator */ -.milestone-badge { - display: inline-flex; - align-items: center; - gap: 3px; - font-family: var(--font-mono); - font-size: 0.65rem; - background: #121d26; - border: 1px solid #233546; - border-radius: 4px; - padding: 1px 6px; - color: var(--neon-cyan); -} - -/* Trophy Cabinet & Achievements */ -.trophy-card { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 12px 14px; +/* Left Sidebar */ +#desktop-nav-sidebar { + width: 230px; + flex-shrink: 0; display: flex; flex-direction: column; - gap: 8px; - position: relative; - overflow: hidden; + gap: 12px; } -.trophy-card.completed { - border-color: #22c55e; - background: #0d1a14; +.sidebar-section-title { + font-size: 0.72rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0 4px; } -.trophy-card.claimable { - border-color: var(--brand-gold); - box-shadow: 0 0 12px rgba(234, 179, 8, 0.3); - animation: glow-gold 2s infinite alternate; -} - -@keyframes glow-gold { - from { box-shadow: 0 0 6px rgba(234, 179, 8, 0.2); } - to { box-shadow: 0 0 16px rgba(234, 179, 8, 0.5); } -} - -.trophy-header { +.sidebar-nav { display: flex; - justify-content: space-between; + 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-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + font-size: 0.78rem; + font-weight: 600; + color: var(--text-secondary); + border-radius: var(--radius-sm); +} + +.sidebar-sector-item iconify-icon { + font-size: 1rem; +} + +/* 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; +} + +/* ========================================================================== + PRODUCTION FLOOR & FACILITY CARDS + ========================================================================== */ + +.production-toolbar { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 8px 12px; +} + +.sector-tabs-scroll { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 2px; +} + +.sector-pill { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--bg-surface-sub); + border: 1px solid var(--border-color); + padding: 6px 12px; + border-radius: var(--radius-sm); + font-size: 0.78rem; + font-weight: 600; + color: var(--text-secondary); + cursor: pointer; + white-space: nowrap; + transition: all 0.15s; +} + +.sector-pill:hover { + border-color: var(--text-muted); +} + +.sector-pill.active { + background: var(--text-primary); + color: #ffffff; + border-color: var(--text-primary); +} + +.production-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 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; + gap: 12px; + cursor: pointer; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; +} + +.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; } -.trophy-stars { - color: var(--brand-gold); - font-size: 0.85rem; - letter-spacing: 2px; +.facility-icon-wrap { + width: 44px; + height: 44px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; } -.trophy-progress-track { +.icon-lg { + font-size: 1.6rem; +} + +.facility-title-wrap { + flex: 1; + min-width: 0; +} + +.facility-meta-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2px; +} + +.sector-badge { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.facility-name { + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); + line-height: 1.2; +} + +.facility-type-sub { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.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; +} + +.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: #161e27; + background: var(--bg-surface-sub); border-radius: 3px; overflow: hidden; } -.trophy-progress-fill { +.cycle-bar-fill { height: 100%; - background: linear-gradient(90deg, #06b6d4, #22c55e); - transition: width 0.3s ease; + transition: width 0.1s linear; } -.btn-claim-reward { - background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%); - color: #000; - font-weight: 800; - font-family: var(--font-mono); - border: none; - border-radius: 8px; - padding: 8px 12px; - cursor: pointer; - text-align: center; - box-shadow: 0 0 12px rgba(234, 179, 8, 0.6); - transition: transform 0.1s, filter 0.15s; -} - -.btn-claim-reward:active { - transform: scale(0.96); -} - -/* Swiss Offshore Perk Shop */ -.perk-grid { +/* Facility Stats Grid */ +.facility-stats-grid { display: grid; - grid-template-columns: 1fr; - gap: 10px; - margin-top: 10px; -} - -.perk-card { - background: #0d1217; - border: 1px solid #1f2a36; - border-radius: 10px; - padding: 12px; - display: flex; - flex-direction: column; + grid-template-columns: 1fr 1fr 1fr; gap: 8px; -} - -.perk-card-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.perk-lvl-badge { - font-family: var(--font-mono); - font-size: 0.7rem; - font-weight: bold; - color: var(--neon-cyan); - background: #101c26; - padding: 2px 8px; - border-radius: 4px; - border: 1px solid #1a3246; -} - -.btn-perk-buy { - background: #122332; - border: 1px solid #234562; - color: var(--neon-cyan); - padding: 8px; - border-radius: 6px; - font-family: var(--font-mono); - font-weight: 700; - font-size: 0.75rem; - cursor: pointer; - transition: background 0.15s; -} - -.btn-perk-buy:hover:not(:disabled) { - background: #18334a; -} - -.btn-perk-buy:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -/* Clever Helpers Styling */ -.helper-grid { - display: flex; - flex-direction: column; - gap: 12px; -} - -.helper-card { - background: #0d131a; - border: 1px solid #1f2e3d; - border-radius: 12px; - padding: 14px; - display: flex; - flex-direction: column; - gap: 10px; - position: relative; - overflow: hidden; -} - -.helper-card.unlocked { - border-color: #2563eb; - box-shadow: 0 0 16px rgba(37, 99, 235, 0.12); -} - -.helper-card-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.helper-status-badge { - font-family: var(--font-mono); - font-size: 0.68rem; - font-weight: 700; - padding: 3px 8px; - border-radius: 4px; -} - -.helper-status-badge.active { - background: #064e3b; - color: #34d399; - border: 1px solid #059669; -} - -.helper-status-badge.locked { - background: #1f2937; - color: #9ca3af; - border: 1px solid #374151; -} - -/* ROI Advisor Dashboard */ -.roi-dashboard { - background: #090e14; - border: 1px solid #1c2b38; - border-radius: 10px; - padding: 12px; - display: flex; - flex-direction: column; - gap: 10px; -} - -.roi-target-highlight { - background: #111e2e; - border: 1px solid #38bdf8; - border-radius: 8px; - padding: 10px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.roi-rank-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 8px; - border-bottom: 1px solid #16202a; - font-family: var(--font-mono); - font-size: 0.72rem; -} - -.roi-rank-item:last-child { - border-bottom: none; -} - -/* Portfolio Allocation Buttons (10%, 25%, 50%, 100%) */ -.alloc-btn-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 6px; - margin-top: 6px; -} - -.btn-alloc { - background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); - border: 1px solid #334155; - color: #e2e8f0; - font-family: var(--font-mono); - font-size: 0.72rem; - font-weight: 700; - padding: 8px 4px; - border-radius: 6px; - cursor: pointer; - text-align: center; - transition: all 0.15s; -} - -.btn-alloc:hover:not(:disabled) { - border-color: #38bdf8; - color: #38bdf8; - background: #1e3a5f; -} - -.btn-alloc:active:not(:disabled) { - transform: scale(0.95); -} - -.btn-alloc:disabled { - opacity: 0.35; - cursor: not-allowed; -} - -/* Main Viewport Content */ -#main-viewport { - flex: 1; - padding: 12px 14px; - display: flex; - flex-direction: column; - gap: 12px; -} - -/* Bottom Tab Navigation Bar - Fixed at bottom of screen */ -#bottom-nav { - position: fixed; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 100%; - max-width: 640px; - height: 68px; - background: rgba(11, 14, 18, 0.97); - backdrop-filter: blur(12px); - border-top: 1px solid var(--border-color); - display: flex; - justify-content: space-around; - align-items: center; - z-index: 50; - padding-bottom: env(safe-area-inset-bottom, 4px); - box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.5); -} - -.nav-tab { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 3px; - color: var(--text-secondary); - background: none; - border: none; - cursor: pointer; - font-size: 0.72rem; - font-family: var(--font-mono); - padding: 8px 0; - transition: color 0.15s, transform 0.1s; -} - -.nav-tab:active { - transform: scale(0.92); -} - -.nav-tab.active { - color: var(--brand-gold); -} - -.nav-tab.active iconify-icon { - filter: drop-shadow(0 0 6px var(--brand-gold)); -} - -.nav-tab iconify-icon { - font-size: 1.5rem; -} - -/* Card Components */ -.card { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 12px 14px; - position: relative; - overflow: hidden; - transition: border-color 0.2s, background-color 0.2s; -} - -.card:hover { - border-color: var(--border-accent); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -.card-title-group { - display: flex; - align-items: center; - gap: 10px; -} - -.drug-icon-box { - width: 38px; - height: 38px; - border-radius: 8px; - background: var(--bg-card-sub); - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--border-color); - flex-shrink: 0; -} - -.drug-icon-box iconify-icon { - font-size: 1.55rem; -} - -.card-title { - font-size: 0.98rem; - font-weight: 700; - letter-spacing: 0.3px; -} - -.card-stats-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - background: var(--bg-card-sub); - border-radius: 8px; + background: var(--bg-surface-sub); + border-radius: var(--radius-sm); padding: 8px 10px; - margin-bottom: 10px; - font-family: var(--font-mono); - font-size: 0.78rem; } -.stat-item { +.stat-cell { display: flex; flex-direction: column; } .stat-label { - font-size: 0.65rem; - color: var(--text-secondary); - text-transform: uppercase; + font-size: 0.68rem; + font-weight: 600; + color: var(--text-muted); } .stat-value { + font-size: 0.8rem; + color: var(--text-primary); +} + +/* 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; + 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; +} + +.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; +} + +.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); +} + +/* ========================================================================== + 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); } -/* Touch-Optimized Action Buttons */ -.btn-grid-actions { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 6px; - margin-bottom: 8px; -} - -.btn-grid-auto { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 6px; -} - -.btn-tactical { - background: #181f26; - color: var(--text-primary); - border: 1px solid #2d3844; - border-radius: 8px; - padding: 9px 4px; - font-family: var(--font-mono); - font-size: 0.74rem; +.client-badge { + font-size: 0.7rem; font-weight: 600; - cursor: pointer; - display: flex; - flex-direction: column; + color: var(--text-muted); +} + +.contract-timer { + display: inline-flex; align-items: center; - justify-content: center; - gap: 2px; - transition: background 0.12s, border-color 0.12s, transform 0.05s; - text-align: center; - min-height: 48px; /* High tactile accessibility for thumbs */ -} - -.btn-tactical:active:not(:disabled) { - transform: scale(0.95); - background: #25313d; -} - -.btn-tactical:disabled { - opacity: 0.4; - cursor: not-allowed; - filter: grayscale(0.7); -} - -.btn-subtext { - font-size: 0.62rem; + 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); } -.btn-primary-action { - background: #172c1f; - border-color: #2b5637; - color: #4ade80; +.timer-urgent { + background: var(--amber-light); + color: var(--amber-dark); } -.btn-primary-action:hover:not(:disabled) { - background: #203f2c; +.contract-title { + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; } -.btn-sell-action { - background: #2b2310; - border-color: #5c4b1a; - color: #facc15; -} - -/* Locked Card State */ -.card.locked { - background: #0d1014; - border-style: dashed; -} - -.unlock-overlay-box { +.contract-req-row { display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; - padding: 16px 0; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 6px; } -.btn-unlock { - background: #201a08; - border: 1px solid var(--brand-gold); - color: var(--brand-gold); - padding: 11px 20px; - border-radius: 8px; - font-family: var(--font-mono); - font-size: 0.86rem; - font-weight: bold; - cursor: pointer; +.req-label { + font-size: 0.72rem; + color: var(--text-muted); +} + +.req-val-wrap { display: flex; align-items: center; gap: 6px; - box-shadow: 0 0 14px rgba(234, 179, 8, 0.15); + font-size: 0.85rem; } -/* Modal Windows (Auth & Raids) */ -.modal-overlay { - position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.88); - backdrop-filter: blur(6px); - display: none; - align-items: center; - justify-content: center; - z-index: 100; - padding: 20px; -} - -.modal-overlay.active { - display: flex; -} - -.modal-content { - background: #10151c; - border: 2px solid var(--border-accent); - border-radius: 16px; +.contract-progress-track { width: 100%; - max-width: 400px; - padding: 22px; + 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); +} + +/* ========================================================================== + BOARDROOM & IPO VIEW + ========================================================================== */ + +.boardroom-header-pane { display: flex; flex-direction: column; - gap: 14px; - box-shadow: 0 0 35px rgba(0,0,0,0.9); + gap: 16px; } -.modal-header { +.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; } -.modal-title { - font-family: var(--font-mono); - font-size: 1.1rem; - font-weight: bold; +.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); } -.input-group { +.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; +} + +/* ========================================================================== + 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; + gap: 8px; +} + +.liquidation-buttons button { + flex: 1; + font-size: 0.75rem; + padding: 8px 6px; +} + +.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: 4px; } -.input-label { - font-size: 0.7rem; - color: var(--text-secondary); - font-family: var(--font-mono); - text-transform: uppercase; -} - -.input-field { - background: #090c0f; +.chain-item-row { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--bg-surface); border: 1px solid var(--border-color); - border-radius: 8px; - padding: 10px 12px; - color: #fff; - font-family: var(--font-mono); - font-size: 0.9rem; - outline: none; + padding: 6px 8px; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background 0.15s; } -.input-field:focus { - border-color: var(--brand-gold); +.chain-item-row:hover { + background: var(--primary-blue-light); } -/* Honeypot field: invisible to human users, accessible to bots */ -.hp-trap { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - position: absolute !important; - left: -9999px !important; +.chain-item-left { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.78rem; + font-weight: 600; } -/* Toast Messages */ +.chain-item-ratio { + font-size: 0.7rem; + color: var(--text-muted); +} + +.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); +} + +/* ========================================================================== + 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); +} + +/* ========================================================================== + 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; - top: 76px; - left: 50%; - transform: translateX(-50%); - width: 90%; - max-width: 580px; - z-index: 80; + bottom: 20px; + right: 20px; + z-index: 1000; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } -.toast { - background: rgba(16, 21, 28, 0.96); - border: 1px solid var(--border-accent); - border-left: 4px solid var(--neon-green); - border-radius: 8px; - padding: 9px 12px; - font-size: 0.8rem; - font-family: var(--font-mono); +@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); - box-shadow: 0 4px 16px rgba(0,0,0,0.6); - animation: slideDown 0.25s ease; -} - -.toast.danger { - border-left-color: var(--neon-red); -} - -@keyframes slideDown { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } } diff --git a/frontend/index.html b/frontend/index.html index 771440f..97de587 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,265 +3,216 @@ - EL PURO - + Production - The Idle Game + + - - + + + + -
+
- -
+ +
- - - - - - - - - - -