From ab5f97acbf7f8d69c3756a6c0cbd6b93f4fb2a1f Mon Sep 17 00:00:00 2001 From: madgerm Date: Fri, 24 Apr 2026 15:53:59 +0200 Subject: [PATCH] feat(phase1): complete base structure - landing page, auth, dashboard, airports, API, DB schema, 60 airports CSV --- database/schema.sql | 116 ++++ www/data/airports.csv | 61 ++ www/public/airports.php | 373 +++++++++++ www/public/api/airports.php | 142 +++++ www/public/api/auth.php | 189 ++++++ www/public/api/game.php | 331 ++++++++++ www/public/assets/css/style.css | 1050 +++++++++++++++++++++++++++++++ www/public/assets/js/main.js | 865 +++++++++++++++++++++++++ www/public/dashboard.php | 461 ++++++++++++++ www/public/index.php | 203 ++++++ www/public/login.php | 134 ++++ www/public/logout.php | 11 + www/public/register.php | 207 ++++++ www/src/Database.php | 373 +++++++++++ www/src/Router.php | 78 +++ www/src/Session.php | 121 ++++ www/src/bootstrap.php | 122 ++++ www/src/config.php | 89 +++ 18 files changed, 4926 insertions(+) create mode 100644 database/schema.sql create mode 100644 www/data/airports.csv create mode 100644 www/public/airports.php create mode 100644 www/public/api/airports.php create mode 100644 www/public/api/auth.php create mode 100644 www/public/api/game.php create mode 100644 www/public/assets/css/style.css create mode 100644 www/public/assets/js/main.js create mode 100644 www/public/dashboard.php create mode 100644 www/public/index.php create mode 100644 www/public/login.php create mode 100644 www/public/logout.php create mode 100644 www/public/register.php create mode 100644 www/src/Database.php create mode 100644 www/src/Router.php create mode 100644 www/src/Session.php create mode 100644 www/src/bootstrap.php create mode 100644 www/src/config.php diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..4bfb6e9 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,116 @@ +-- Airline Tycoon Browser-MMO Datenbankschema (SQLite) +-- Phase 1: User, Airlines, Flughäfen, Basis-Routen + +-- Benutzer (Spieler) +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + airline_name VARCHAR(100) DEFAULT '', + balance DECIMAL(15,2) DEFAULT 500000.00, + level INTEGER DEFAULT 1, + experience INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1 +); + +-- Flughäfen (statisch, geladen aus CSV) +CREATE TABLE IF NOT EXISTS airports ( + id INTEGER PRIMARY KEY, + iata_code VARCHAR(3) UNIQUE NOT NULL, + name VARCHAR(200) NOT NULL, + city VARCHAR(100) NOT NULL, + country VARCHAR(100) NOT NULL, + continent VARCHAR(50) NOT NULL, + latitude DECIMAL(10,6) NOT NULL, + longitude DECIMAL(10,6) NOT NULL, + timezone VARCHAR(50) DEFAULT 'UTC', + passenger_volume INTEGER DEFAULT 1000000, + demand_multiplier DECIMAL(3,2) DEFAULT 1.00 +); + +-- Flugzeuge (Fleet) +CREATE TABLE IF NOT EXISTS aircraft ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + aircraft_type VARCHAR(50) NOT NULL, + name VARCHAR(100) DEFAULT '', + condition DECIMAL(5,2) DEFAULT 100.00, + flights_count INTEGER DEFAULT 0, + purchase_price DECIMAL(15,2) NOT NULL, + seat_capacity INTEGER NOT NULL, + range_km INTEGER NOT NULL, + fuel_efficiency DECIMAL(5,2) DEFAULT 1.00, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- Routen +CREATE TABLE IF NOT EXISTS routes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + from_airport_id INTEGER NOT NULL, + to_airport_id INTEGER NOT NULL, + distance_km INTEGER NOT NULL, + base_price DECIMAL(10,2) DEFAULT 100.00, + demand_level INTEGER DEFAULT 50, + is_active BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (from_airport_id) REFERENCES airports(id), + FOREIGN KEY (to_airport_id) REFERENCES airports(id) +); + +-- Flüge (durchgeführte Flüge) +CREATE TABLE IF NOT EXISTS flights ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + route_id INTEGER NOT NULL, + aircraft_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + departure_time DATETIME NOT NULL, + arrival_time DATETIME NOT NULL, + passengers INTEGER DEFAULT 0, + revenue DECIMAL(15,2) DEFAULT 0.00, + fuel_cost DECIMAL(15,2) DEFAULT 0.00, + profit DECIMAL(15,2) DEFAULT 0.00, + status VARCHAR(20) DEFAULT 'completed', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (route_id) REFERENCES routes(id), + FOREIGN KEY (aircraft_id) REFERENCES aircraft(id), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- Statistiken (tägliche Auswertungen) +CREATE TABLE IF NOT EXISTS daily_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + date DATE NOT NULL, + total_flights INTEGER DEFAULT 0, + total_passengers INTEGER DEFAULT 0, + total_revenue DECIMAL(15,2) DEFAULT 0.00, + total_profit DECIMAL(15,2) DEFAULT 0.00, + fuel_cost DECIMAL(15,2) DEFAULT 0.00, + UNIQUE(user_id, date), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- Events (Werbeaktionen, Wettbewerbe) +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title VARCHAR(200) NOT NULL, + description TEXT, + event_type VARCHAR(50) DEFAULT 'special', + bonus_multiplier DECIMAL(3,2) DEFAULT 1.00, + start_date DATETIME NOT NULL, + end_date DATETIME NOT NULL, + is_active BOOLEAN DEFAULT 1 +); + +-- Indexes für Performance +CREATE INDEX IF NOT EXISTS idx_routes_user ON routes(user_id); +CREATE INDEX IF NOT EXISTS idx_flights_user ON flights(user_id); +CREATE INDEX IF NOT EXISTS idx_flights_route ON flights(route_id); +CREATE INDEX IF NOT EXISTS idx_aircraft_user ON aircraft(user_id); +CREATE INDEX IF NOT EXISTS idx_airports_iata ON airports(iata_code); diff --git a/www/data/airports.csv b/www/data/airports.csv new file mode 100644 index 0000000..92e61a4 --- /dev/null +++ b/www/data/airports.csv @@ -0,0 +1,61 @@ +id;iata_code;name;city;country;continent;latitude;longitude;timezone;passenger_volume;demand_multiplier +1;FRA;Frankfurt Airport;Frankfurt;Germany;Europe;50.0379;8.5622;Europe/Berlin;70000000;1.50 +2;MUC;Munich Airport;Munich;Germany;Europe;48.3537;11.7861;Europe/Berlin;50000000;1.30 +3;CDG;Charles de Gaulle Airport;Paris;France;Europe;49.0097;2.5479;Europe/Paris;76000000;1.40 +4;AMS;Amsterdam Airport Schiphol;Amsterdam;Netherlands;Europe;52.3105;4.7683;Europe/Amsterdam;72000000;1.35 +5;LHR;London Heathrow Airport;London;UK;Europe;51.4700;-0.4543;Europe/London;80000000;1.45 +6;MAD;Adolfo Suárez Madrid-Barajas Airport;Madrid;Spain;Europe;40.4983;-3.5676;Europe/Madrid;60000000;1.25 +7;BCN;Barcelona-El Prat Airport;Barcelona;Spain;Europe;41.2974;2.0833;Europe/Madrid;50000000;1.30 +8;FCO;Leonardo da Vinci-Fiumicino Airport;Rome;Italy;Europe;41.8003;12.2389;Europe/Rome;49000000;1.25 +9;ZRH;Zurich Airport;Zurich;Switzerland;Europe;47.4647;8.5492;Europe/Zurich;31000000;1.20 +10;VIE;Vienna International Airport;Vienna;Austria;Europe;48.1103;16.5697;Europe/Vienna;31000000;1.15 +11;CPH;Copenhagen Airport;Copenhagen;Denmark;Europe;55.6180;12.6560;Europe/Copenhagen;30000000;1.15 +12;OSL;Oslo Airport, Gardermoen;Oslo;Norway;Europe;60.1939;11.1004;Europe/Oslo;29000000;1.10 +13;ARN;Stockholm Arlanda Airport;Stockholm;Sweden;Europe;59.6519;17.9186;Europe/Stockholm;28000000;1.10 +14;HEL;Helsinki-Vantaa Airport;Helsinki;Finland;Europe;60.3172;24.9633;Europe/Helsinki;22000000;1.05 +15;WAW;Warsaw Chopin Airport;Warsaw;Poland;Europe;52.1657;20.9671;Europe/Warsaw;19000000;1.10 +16;JFK;John F. Kennedy International Airport;New York;USA;North America;40.6413;-73.7781;America/New_York;62000000;1.60 +17;LAX;Los Angeles International Airport;Los Angeles;USA;North America;33.9425;-118.4081;America/Los_Angeles;87000000;1.55 +18;ORD;O'Hare International Airport;Chicago;USA;North America;41.9742;-87.9073;America/Chicago;84000000;1.50 +19;ATL;Hartsfield-Jackson Atlanta International Airport;Atlanta;USA;North America;33.6407;-84.4277;America/New_York;107000000;1.45 +20;DFW;Dallas/Fort Worth International Airport;Dallas;USA;North America;32.8998;-97.0403;America/Chicago;75000000;1.40 +21;DEN;Denver International Airport;Denver;USA;North America;39.8561;-104.6737;America/Denver;69000000;1.35 +22;SFO;San Francisco International Airport;San Francisco;USA;North America;37.6213;-122.3790;America/Los_Angeles;58000000;1.45 +23;SEA;Seattle-Tacoma International Airport;Seattle;USA;North America;47.4502;-122.3088;America/Los_Angeles;51000000;1.30 +24;MIA;Miami International Airport;Miami;USA;North America;25.7959;-80.2870;America/New_York;45000000;1.40 +25;BOS;Boston Logan International Airport;Boston;USA;North America;42.3656;-71.0096;America/New_York;42000000;1.30 +26;MEX;Mexico City International Airport;Mexico City;Mexico;North America;19.4363;-99.0721;America/Mexico_City;50000000;1.35 +27;YYZ;Toronto Pearson International Airport;Toronto;Canada;North America;43.6777;-79.6248;America/Toronto;50000000;1.30 +28;YVR;Vancouver International Airport;Vancouver;Canada;North America;49.1967;-123.1815;America/Vancouver;26000000;1.20 +29;GRU;São Paulo/Guarulhos International Airport;São Paulo;Brazil;South America;-23.4356;-46.4731;America/Sao_Paulo;43000000;1.40 +30;EZE;Ministro Pistarini International Airport;Buenos Aires;Argentina;South America;-34.8222;-58.5358;America/Argentina/Buenos_Aires;13000000;1.20 +31;BOG;El Dorado International Airport;Bogotá;Colombia;South America;4.7016;-74.1469;America/Bogota;35000000;1.25 +32;LIM;Jorge Chávez International Airport;Lima;Peru;South America;-12.0219;-77.1143;America/Lima;22000000;1.15 +33;UIO;Mariscal Sucre International Airport;Quito;Ecuador;South America;-0.1412;-78.4892;America/Guayaquil;7000000;1.00 +34;SYD;Sydney Kingsford Smith Airport;Sydney;Australia;Australia;-33.9399;151.1753;Australia/Sydney;44000000;1.35 +35;MEL;Melbourne Airport;Melbourne;Australia;Australia;-37.6690;144.8410;Australia/Melbourne;37000000;1.30 +36;SIN;Singapore Changi Airport;Singapore;Singapore;Asia;1.3644;103.9915;Asia/Singapore;65000000;1.55 +37;HKG;Hong Kong International Airport;Hong Kong;Hong Kong;Asia;22.3080;113.9185;Asia/Hong_Kong;75000000;1.50 +38;NRT;Narita International Airport;Tokyo;Japan;Asia;35.7720;140.3929;Asia/Tokyo;43000000;1.45 +39;ICN;Incheon International Airport;Seoul;South Korea;Asia;37.4602;126.4407;Asia/Seoul;72000000;1.50 +40;PEK;Beijing Capital International Airport;Beijing;China;Asia;40.0799;116.6031;Asia/Shanghai;100000000;1.60 +41;PVG;Shanghai Pudong International Airport;Shanghai;China;Asia;31.1443;121.8083;Asia/Shanghai;74000000;1.50 +42;CAN;Guangzhou Baiyun International Airport;Guangzhou;China;Asia;23.3924;113.2988;Asia/Shanghai;73000000;1.45 +43;SZX;Shenzhen Bao'an International Airport;Shenzhen;China;Asia;22.6393;113.8108;Asia/Shanghai;50000000;1.40 +44;BKK;Suvarnabhumi Airport;Bangkok;Thailand;Asia;13.6900;100.7501;Asia/Bangkok;60000000;1.40 +45;KUL;Kuala Lumpur International Airport;Kuala Lumpur;Malaysia;Asia;2.7456;101.7072;Asia/Kuala_Lumpur;59000000;1.35 +46;DEL;Indira Gandhi International Airport;New Delhi;India;Asia;28.5562;77.1000;Asia/Kolkata;69000000;1.45 +47;BOM;Chhatrapati Shivaji Maharaj International Airport;Mumbai;India;Asia;19.0896;72.8656;Asia/Kolkata;49000000;1.40 +48;DXB;Dubai International Airport;Dubai;UAE;Asia;25.2532;55.3657;Asia/Dubai;89000000;1.60 +49;AUH;Abu Dhabi International Airport;Abu Dhabi;UAE;Asia;24.4330;54.6511;Asia/Dubai;30000000;1.30 +50;DOH;Hamad International Airport;Doha;Qatar;Asia;25.2609;51.6138;Asia/Qatar;53000000;1.40 +51;IST;Istanbul Airport;Istanbul;Turkey;Europe;41.2753;28.7519;Europe/Istanbul;76000000;1.45 +52;ATH;Athens International Airport;Athens;Greece;Europe;37.9364;23.9445;Europe/Athens;25000000;1.15 +53;TLV;Ben Gurion International Airport;Tel Aviv;Israel;Asia;32.0055;34.8854;Asia/Jerusalem;25000000;1.20 +54;CAI;Cairo International Airport;Cairo;Egypt;Africa;30.1219;31.4056;Africa/Cairo;30000000;1.25 +55;JNB;O. R. Tambo International Airport;Johannesburg;South Africa;Africa;-26.1392;28.2460;Africa/Johannesburg;22000000;1.20 +56;CPT;Cape Town International Airport;Cape Town;South Africa;Africa;-33.9715;18.6021;Africa/Johannesburg;10000000;1.10 +57;NBO;Jomo Kenyatta International Airport;Nairobi;Kenya;Africa;-1.3192;36.9275;Africa/Nairobi;8000000;1.05 +58;CMN;Mohammed V International Airport;Casablanca;Morocco;Africa;33.3675;-7.5900;Africa/Casablanca;9000000;1.10 +59;LOS;Murtala Muhammed International Airport;Lagos;Nigeria;Africa;6.5774;3.3212;Africa/Lagos;7000000;1.05 +60;ADD;Addis Ababa Bole International Airport;Addis Ababa;Ethiopia;Africa;8.9779;38.7993;Africa/Addis_Ababa;12000000;1.15 diff --git a/www/public/airports.php b/www/public/airports.php new file mode 100644 index 0000000..a08d282 --- /dev/null +++ b/www/public/airports.php @@ -0,0 +1,373 @@ +getUserById($userId); + +if (!$user) { + Session::logout(); + redirect('/login.php'); +} + +Session::refreshUserData($db); +$user = $db->getUserById($userId); + +// Airports laden falls noch nicht in DB +$airports = $db->getAllAirports(); +if (empty($airports)) { + $db->loadAirportsFromCsv($config['paths']['data'] . '/airports.csv'); + $airports = $db->getAllAirports(); +} + +$aircraft = $db->getUserAircraft($userId); +$routes = $db->getUserRoutes($userId); + +// Route erstellen +$message = ''; +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_route') { + if (!validate_csrf($_POST['csrf_token'] ?? '')) { + $message = 'Ungültige Anfrage.'; + } else { + $fromId = (int) ($_POST['from_airport'] ?? 0); + $toId = (int) ($_POST['to_airport'] ?? 0); + + if ($fromId > 0 && $toId > 0 && $fromId !== $toId) { + $fromAirport = $db->selectOne("SELECT * FROM airports WHERE id = ?", [$fromId]); + $toAirport = $db->selectOne("SELECT * FROM airports WHERE id = ?", [$toId]); + + if ($fromAirport && $toAirport) { + $distance = $db->calculateDistance( + $fromAirport['latitude'], $fromAirport['longitude'], + $toAirport['latitude'], $toAirport['longitude'] + ); + + // Prüfen ob Route bereits existiert + $existing = $db->selectOne( + "SELECT id FROM routes WHERE user_id = ? AND from_airport_id = ? AND to_airport_id = ?", + [$userId, $fromId, $toId] + ); + + if ($existing) { + $message = 'Diese Route existiert bereits!'; + } else { + $db->createRoute($userId, $fromId, $toId, $distance); + $message = 'Route erfolgreich erstellt!'; + } + } + } else { + $message = 'Bitte wähle zwei verschiedene Flughäfen.'; + } + } +} +?> + + + + + + + + Weltkarte - Airline Tycoon + + + + + + + +
+ +
+

ℹ️

+
+ + + + + + + +
+

🗺️ Interaktive Weltkarte

+

+ Klicke oder tippe auf einen Flughafen um eine Route zu erstellen. +

+ +
+ +
+ +
+ + + +
+
+ + +
+

🛫 Neue Route erstellen

+ +
+ + + +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+ + +
+

📋 Alle Flughäfen ()

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
IATANameStadtLandKontinentPassagiere/Jahr
+
+
+ + + +
+

✈️ Meine Flotte

+ + +
+

Du hast noch keine Flugzeuge.

+ + 🛒 Flugzeug kaufen + +
+ +
+ +
+
+
✈️
+
+

+

+
+
+ +
+
+ Sitze: +
+
+ Reichweite: km +
+
+ Zustand: + + % + +
+
+ Flüge: +
+
+
+ +
+ +
+ + + +
+

🛫 Meine Routen

+ + +
+

Du hast noch keine Routen.

+ + 🗺️ Route erstellen + +
+ +
+ +
+
+
+
+ +
+
✈️
+
+
+ +
+
+ +
+ Distanz: + km +
+
+ Nachfrage: +
+
+ % +
+
+
+
+ +
+ +
+ +
+ + +
+ + + + + + diff --git a/www/public/api/airports.php b/www/public/api/airports.php new file mode 100644 index 0000000..e409e0c --- /dev/null +++ b/www/public/api/airports.php @@ -0,0 +1,142 @@ + 'Unbekannte Aktion'], 400); +} + +function handleList() { + global $db; + + // Airports aus CSV laden falls nötig + $airports = $db->getAllAirports(); + if (empty($airports)) { + $config = require dirname(__DIR__) . '/src/config.php'; + $count = $db->loadAirportsFromCsv($config['paths']['data'] . '/airports.csv'); + $airports = $db->getAllAirports(); + } + + // Optional filtern + $continent = $_GET['continent'] ?? null; + $country = $_GET['country'] ?? null; + + if ($continent) { + $airports = array_filter($airports, fn($a) => $a['continent'] === $continent); + } + + if ($country) { + $airports = array_filter($airports, fn($a) => $a['country'] === $country); + } + + jsonResponse([ + 'success' => true, + 'airports' => array_values($airports), + 'count' => count($airports) + ]); +} + +function handleGet() { + global $db; + + $id = (int) ($_GET['id'] ?? 0); + $iata = $_GET['iata'] ?? ''; + + if ($id > 0) { + $airport = $db->selectOne("SELECT * FROM airports WHERE id = ?", [$id]); + } elseif ($iata) { + $airport = $db->getAirportByIata($iata); + } else { + jsonResponse(['error' => 'ID oder IATA-Code erforderlich'], 400); + } + + if (!$airport) { + jsonResponse(['error' => 'Flughafen nicht gefunden'], 404); + } + + jsonResponse([ + 'success' => true, + 'airport' => $airport + ]); +} + +function handleSearch() { + global $db; + + $query = trim($_GET['q'] ?? ''); + + if (strlen($query) < 2) { + jsonResponse(['error' => 'Suchbegriff zu kurz (min 2 Zeichen)'], 400); + } + + $query = '%' . $query . '%'; + + $airports = $db->select( + "SELECT * FROM airports + WHERE name LIKE ? OR city LIKE ? OR iata_code LIKE ? OR country LIKE ? + ORDER BY name + LIMIT 20", + [$query, $query, $query, $query] + ); + + jsonResponse([ + 'success' => true, + 'airports' => $airports, + 'count' => count($airports) + ]); +} + +function handleDistance() { + global $db; + + $from = (int) ($_GET['from'] ?? 0); + $to = (int) ($_GET['to'] ?? 0); + + if ($from <= 0 || $to <= 0) { + jsonResponse(['error' => 'Gültige Flughafen-IDs erforderlich'], 400); + } + + $fromAirport = $db->selectOne("SELECT * FROM airports WHERE id = ?", [$from]); + $toAirport = $db->selectOne("SELECT * FROM airports WHERE id = ?", [$to]); + + if (!$fromAirport || !$toAirport) { + jsonResponse(['error' => 'Ein oder beide Flughäfen nicht gefunden'], 404); + } + + $distance = $db->calculateDistance( + $fromAirport['latitude'], $fromAirport['longitude'], + $toAirport['latitude'], $toAirport['longitude'] + ); + + jsonResponse([ + 'success' => true, + 'distance' => $distance, + 'from' => $fromAirport, + 'to' => $toAirport + ]); +} diff --git a/www/public/api/auth.php b/www/public/api/auth.php new file mode 100644 index 0000000..cdcb2e4 --- /dev/null +++ b/www/public/api/auth.php @@ -0,0 +1,189 @@ + 'Nur AJAX-Anfragen erlaubt'], 403); +} + +$action = $_GET['action'] ?? $_POST['action'] ?? ''; + +switch ($action) { + case 'login': + handleLogin(); + break; + + case 'register': + handleRegister(); + break; + + case 'logout': + handleLogout(); + break; + + case 'me': + handleMe(); + break; + + case 'check': + handleCheck(); + break; + + default: + jsonResponse(['error' => 'Unbekannte Aktion'], 400); +} + +function handleLogin() { + global $db; + + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + + if (empty($username) || empty($password)) { + jsonResponse(['error' => 'Bitte fülle alle Felder aus'], 400); + } + + $user = $db->getUserByUsername($username); + + if ($user && password_verify($password, $user['password_hash'])) { + Session::login($user); + + jsonResponse([ + 'success' => true, + 'message' => 'Anmeldung erfolgreich!', + 'redirect' => BASE_PATH . 'dashboard.php', + 'user' => [ + 'id' => $user['id'], + 'username' => $user['username'], + 'airline_name' => $user['airline_name'], + 'balance' => $user['balance'], + 'level' => $user['level'] + ] + ]); + } else { + jsonResponse(['error' => 'Benutzername oder Passwort falsch'], 401); + } +} + +function handleRegister() { + global $db, $config; + + $username = trim($_POST['username'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $password = $_POST['password'] ?? ''; + $airline_name = trim($_POST['airline_name'] ?? ''); + + // Validation + if (empty($username) || empty($email) || empty($password)) { + jsonResponse(['error' => 'Bitte fülle alle Pflichtfelder aus'], 400); + } + + if (strlen($username) < 3 || strlen($username) > 50) { + jsonResponse(['error' => 'Benutzername muss zwischen 3 und 50 Zeichen haben'], 400); + } + + if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) { + jsonResponse(['error' => 'Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten'], 400); + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + jsonResponse(['error' => 'Ungültige E-Mail-Adresse'], 400); + } + + if (strlen($password) < 8) { + jsonResponse(['error' => 'Passwort muss mindestens 8 Zeichen haben'], 400); + } + + // Check existing + if ($db->getUserByUsername($username)) { + jsonResponse(['error' => 'Benutzername bereits vergeben'], 409); + } + + if ($db->getUserByEmail($email)) { + jsonResponse(['error' => 'E-Mail-Adresse wird bereits verwendet'], 409); + } + + // Create user + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + $userId = $db->createUser($username, $email, $passwordHash); + + // Set airline name + $finalAirlineName = !empty($airline_name) ? $airline_name : $username . "'s Airlines"; + $db->update('users', ['airline_name' => $finalAirlineName], 'id = ?', [$userId]); + + // Load airports if needed + $airportCount = $db->select("SELECT COUNT(*) as cnt FROM airports")[0]['cnt'] ?? 0; + if ($airportCount == 0) { + $db->loadAirportsFromCsv($config['paths']['data'] . '/airports.csv'); + } + + // Auto login + $user = $db->getUserById($userId); + Session::login($user); + + jsonResponse([ + 'success' => true, + 'message' => 'Registrierung erfolgreich! Willkommen bei ' . $finalAirlineName . '!', + 'redirect' => BASE_PATH . 'dashboard.php', + 'user' => [ + 'id' => $user['id'], + 'username' => $user['username'], + 'airline_name' => $user['airline_name'], + 'balance' => $user['balance'], + 'level' => $user['level'] + ] + ]); +} + +function handleLogout() { + Session::logout(); + + jsonResponse([ + 'success' => true, + 'message' => 'Abmeldung erfolgreich', + 'redirect' => BASE_PATH . 'login.php' + ]); +} + +function handleMe() { + if (!Session::isLoggedIn()) { + jsonResponse(['error' => 'Nicht angemeldet'], 401); + } + + global $db; + $userId = Session::getUserId(); + $user = $db->getUserById($userId); + + if (!$user) { + Session::logout(); + jsonResponse(['error' => 'Benutzer nicht gefunden'], 404); + } + + jsonResponse([ + 'success' => true, + 'user' => [ + 'id' => $user['id'], + 'username' => $user['username'], + 'email' => $user['email'], + 'airline_name' => $user['airline_name'], + 'balance' => $user['balance'], + 'level' => $user['level'], + 'experience' => $user['experience'] + ] + ]); +} + +function handleCheck() { + jsonResponse([ + 'success' => true, + 'authenticated' => Session::isLoggedIn(), + 'csrf_token' => Session::getCsrfToken() + ]); +} diff --git a/www/public/api/game.php b/www/public/api/game.php new file mode 100644 index 0000000..e05b3b1 --- /dev/null +++ b/www/public/api/game.php @@ -0,0 +1,331 @@ + 'Nicht angemeldet'], 401); +} + +switch ($action) { + case 'status': + handleStatus(); + break; + + case 'info': + handleInfo(); + break; + + case 'purchase': + handlePurchase(); + break; + + case 'flight': + handleFlight(); + break; + + case 'routes': + handleRoutes(); + break; + + case 'create_route': + handleCreateRoute(); + break; + + case 'stats': + handleStats(); + break; + + case 'leaderboard': + handleLeaderboard(); + break; + + default: + jsonResponse(['error' => 'Unbekannte Aktion'], 400); +} + +function handleStatus() { + global $db, $config; + + $response = [ + 'success' => true, + 'authenticated' => Session::isLoggedIn(), + 'timestamp' => time(), + 'config' => [ + 'start_balance' => $config['game']['start_balance'], + 'fuel_cost_per_km' => $config['game']['fuel_cost_per_km'] + ] + ]; + + if (Session::isLoggedIn()) { + $userId = Session::getUserId(); + $user = $db->getUserById($userId); + + if ($user) { + $response['user'] = [ + 'id' => $user['id'], + 'username' => $user['username'], + 'airline_name' => $user['airline_name'], + 'balance' => $user['balance'], + 'level' => $user['level'], + 'experience' => $user['experience'] + ]; + } + } + + jsonResponse($response); +} + +function handleInfo() { + global $config; + + jsonResponse([ + 'success' => true, + 'app' => $config['app'], + 'game' => $config['game'], + 'aircraft_types' => $config['aircraft_types'] + ]); +} + +function handlePurchase() { + global $db, $config; + + $userId = Session::getUserId(); + $user = $db->getUserById($userId); + + $type = $_POST['aircraft_type'] ?? ''; + $name = trim($_POST['name'] ?? ''); + + if (empty($type) || !isset($config['aircraft_types'][$type])) { + jsonResponse(['error' => 'Ungültiger Flugzeugtyp'], 400); + } + + $specs = $config['aircraft_types'][$type]; + $price = $specs['purchase_price']; + + if ($user['balance'] < $price) { + jsonResponse([ + 'error' => 'Nicht genug Geld!', + 'required' => $price, + 'available' => $user['balance'] + ], 400); + } + + // Balance abziehen + $newBalance = $user['balance'] - $price; + $db->update('users', ['balance' => $newBalance], 'id = ?', [$userId]); + + // Aircraft erstellen + $aircraftId = $db->purchaseAircraft($userId, $type, $name, $price); + + jsonResponse([ + 'success' => true, + 'message' => "{$type} \"{$name}\" wurde gekauft!", + 'aircraft_id' => $aircraftId, + 'price' => $price, + 'new_balance' => $newBalance + ]); +} + +function handleFlight() { + global $db; + + $userId = Session::getUserId(); + $user = $db->getUserById($userId); + + $routeId = (int) ($_POST['route_id'] ?? 0); + $aircraftId = (int) ($_POST['aircraft_id'] ?? 0); + + if ($routeId <= 0 || $aircraftId <= 0) { + jsonResponse(['error' => 'Route und Flugzeug erforderlich'], 400); + } + + // Route prüfen + $route = $db->selectOne( + "SELECT r.*, a1.name as from_name, a2.name as to_name + FROM routes r + JOIN airports a1 ON r.from_airport_id = a1.id + JOIN airports a2 ON r.to_airport_id = a2.id + WHERE r.id = ? AND r.user_id = ? AND r.is_active = 1", + [$routeId, $userId] + ); + + if (!$route) { + jsonResponse(['error' => 'Route nicht gefunden'], 404); + } + + // Aircraft prüfen + $aircraft = $db->selectOne( + "SELECT * FROM aircraft WHERE id = ? AND user_id = ?", + [$aircraftId, $userId] + ); + + if (!$aircraft) { + jsonResponse(['error' => 'Flugzeug nicht gefunden'], 404); + } + + if ($aircraft['condition'] <= 0) { + jsonResponse(['error' => 'Flugzeug ist zu beschädigt!'], 400); + } + + if ($aircraft['range_km'] < $route['distance_km']) { + jsonResponse([ + 'error' => 'Flugzeug hat nicht genug Reichweite!', + 'required' => $route['distance_km'], + 'available' => $aircraft['range_km'] + ], 400); + } + + try { + $result = $db->executeFlight($userId, $routeId, $aircraftId); + + // User-Daten aktualisieren + $updatedUser = $db->getUserById($userId); + + jsonResponse([ + 'success' => true, + 'message' => 'Flug erfolgreich!', + 'flight' => [ + 'from' => $route['from_name'], + 'to' => $route['to_name'], + 'passengers' => $result['passengers'], + 'revenue' => $result['revenue'], + 'fuel_cost' => $result['fuel_cost'], + 'profit' => $result['profit'] + ], + 'user' => [ + 'balance' => $updatedUser['balance'], + 'experience' => $updatedUser['experience'], + 'level' => $updatedUser['level'] + ] + ]); + } catch (Exception $e) { + jsonResponse(['error' => 'Flug fehlgeschlagen: ' . $e->getMessage()], 500); + } +} + +function handleRoutes() { + global $db; + + $userId = Session::getUserId(); + $routes = $db->getUserRoutes($userId); + + jsonResponse([ + 'success' => true, + 'routes' => $routes, + 'count' => count($routes) + ]); +} + +function handleCreateRoute() { + global $db; + + $userId = Session::getUserId(); + + $fromId = (int) ($_POST['from_airport'] ?? 0); + $toId = (int) ($_POST['to_airport'] ?? 0); + + if ($fromId <= 0 || $toId <= 0) { + jsonResponse(['error' => 'Gültige Flughäfen erforderlich'], 400); + } + + if ($fromId === $toId) { + jsonResponse(['error' => 'Von und Nach müssen unterschiedlich sein'], 400); + } + + // Flughäfen prüfen + $fromAirport = $db->selectOne("SELECT * FROM airports WHERE id = ?", [$fromId]); + $toAirport = $db->selectOne("SELECT * FROM airports WHERE id = ?", [$toId]); + + if (!$fromAirport || !$toAirport) { + jsonResponse(['error' => 'Ein oder beide Flughäfen nicht gefunden'], 404); + } + + // Prüfen ob Route bereits existiert + $existing = $db->selectOne( + "SELECT id FROM routes WHERE user_id = ? AND from_airport_id = ? AND to_airport_id = ?", + [$userId, $fromId, $toId] + ); + + if ($existing) { + jsonResponse(['error' => 'Diese Route existiert bereits'], 409); + } + + // Distance berechnen + $distance = $db->calculateDistance( + $fromAirport['latitude'], $fromAirport['longitude'], + $toAirport['latitude'], $toAirport['longitude'] + ); + + // Route erstellen + $routeId = $db->createRoute($userId, $fromId, $toId, $distance); + + jsonResponse([ + 'success' => true, + 'message' => "Route {$fromAirport['iata_code']} → {$toAirport['iata_code']} erstellt!", + 'route_id' => $routeId, + 'distance' => $distance + ]); +} + +function handleStats() { + global $db; + + $userId = Session::getUserId(); + $user = $db->getUserById($userId); + + $stats = $db->getUserStats($userId); + $todayStats = $db->getTodayStats($userId); + $aircraft = $db->getUserAircraft($userId); + $routes = $db->getUserRoutes($userId); + + // Level-Berechnung + $config = require dirname(__DIR__) . '/src/config.php'; + $expForNextLevel = $config['game']['level_up_threshold'] * pow($config['game']['level_multiplier'], $user['level'] - 1); + + jsonResponse([ + 'success' => true, + 'user' => [ + 'balance' => $user['balance'], + 'level' => $user['level'], + 'experience' => $user['experience'], + 'exp_for_next' => $expForNextLevel + ], + 'stats' => $stats, + 'today' => $todayStats, + 'fleet_count' => count($aircraft), + 'routes_count' => count($routes) + ]); +} + +function handleLeaderboard() { + global $db; + + $limit = min(50, (int) ($_GET['limit'] ?? 10)); + + $leaders = $db->select( + "SELECT id, username, airline_name, balance, level, experience, + (SELECT COUNT(*) FROM routes WHERE user_id = users.id AND is_active = 1) as routes_count, + (SELECT COUNT(*) FROM flights WHERE user_id = users.id) as total_flights + FROM users + WHERE is_active = 1 + ORDER BY balance DESC, level DESC, experience DESC + LIMIT ?", + [$limit] + ); + + jsonResponse([ + 'success' => true, + 'leaderboard' => $leaders, + 'count' => count($leaders) + ]); +} diff --git a/www/public/assets/css/style.css b/www/public/assets/css/style.css new file mode 100644 index 0000000..676b232 --- /dev/null +++ b/www/public/assets/css/style.css @@ -0,0 +1,1050 @@ +/** + * Airline Tycoon - Comic-Style CSS + * Mobile-responsive, Touch-friendly, Dark Mode Support + */ + +/* ============================================ + CSS VARIABLES - LIGHT & DARK THEME + ============================================ */ +:root { + /* Light Theme (Standard) */ + --bg-primary: #f5f0e8; + --bg-secondary: #ffffff; + --bg-tertiary: #e8e0d4; + --text-primary: #2c2416; + --text-secondary: #5a4a32; + --text-muted: #8a7a62; + + /* Comic Farben */ + --comic-yellow: #FFD93D; + --comic-red: #FF6B6B; + --comic-blue: #4ECDC4; + --comic-green: #6BCB77; + --comic-orange: #FF9F43; + --comic-purple: #A855F7; + + /* Accent */ + --accent-primary: #FF6B6B; + --accent-secondary: #4ECDC4; + --accent-gradient: linear-gradient(135deg, #FF6B6B 0%, #4ECDC4 100%); + + /* Schatten */ + --shadow-sm: 2px 2px 0px rgba(0,0,0,0.1); + --shadow-md: 4px 4px 0px rgba(0,0,0,0.15); + --shadow-lg: 6px 6px 0px rgba(0,0,0,0.2); + --shadow-comic: 4px 4px 0px var(--text-primary); + + /* Border */ + --border-thick: 3px solid var(--text-primary); + --border-thin: 2px solid var(--text-primary); + --border-radius: 12px; + --border-radius-sm: 8px; + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + --space-2xl: 48px; + + /* Font */ + --font-primary: 'Bangers', 'Comic Sans MS', cursive; + --font-secondary: 'Patrick Hand', 'Comic Sans MS', cursive; + --font-body: 'Nunito', 'Segoe UI', sans-serif; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; +} + +/* Dark Theme */ +[data-theme="dark"] { + --bg-primary: #1a1a2e; + --bg-secondary: #252542; + --bg-tertiary: #16213e; + --text-primary: #eaeaea; + --text-secondary: #b8b8cc; + --text-muted: #7a7a8c; + --shadow-sm: 2px 2px 0px rgba(0,0,0,0.3); + --shadow-md: 4px 4px 0px rgba(0,0,0,0.4); + --shadow-lg: 6px 6px 0px rgba(0,0,0,0.5); +} + +/* ============================================ + BASE STYLES + ============================================ */ +@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Nunito:wght@400;600;700&family=Patrick+Hand&display=swap'); + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: var(--font-body); + background-color: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; + overflow-x: hidden; +} + +/* Comic Panel Background */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + radial-gradient(circle at 20% 80%, rgba(255, 107, 107, 0.05) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(78, 205, 196, 0.05) 0%, transparent 50%), + url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000000' fill-opacity='0.02'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); + pointer-events: none; + z-index: -1; +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-primary); + letter-spacing: 1px; + line-height: 1.2; + color: var(--text-primary); +} + +h1 { font-size: 2.5rem; } +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } +h4 { font-size: 1.25rem; } + +a { + color: var(--accent-primary); + text-decoration: none; + transition: var(--transition-fast); +} + +a:hover { + color: var(--accent-secondary); +} + +/* ============================================ + COMIC PANELS & CARDS + ============================================ */ +.panel { + background: var(--bg-secondary); + border: var(--border-thick); + border-radius: var(--border-radius); + padding: var(--space-lg); + box-shadow: var(--shadow-comic); + position: relative; +} + +.panel::before { + content: ''; + position: absolute; + top: -8px; + left: -8px; + right: -8px; + bottom: -8px; + border: 2px dashed var(--text-muted); + border-radius: 16px; + opacity: 0.3; + pointer-events: none; +} + +.panel-header { + font-family: var(--font-primary); + font-size: 1.5rem; + margin-bottom: var(--space-md); + padding-bottom: var(--space-sm); + border-bottom: var(--border-thin); + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.card { + background: var(--bg-secondary); + border: var(--border-thin); + border-radius: var(--border-radius-sm); + padding: var(--space-md); + box-shadow: var(--shadow-sm); + transition: var(--transition-normal); +} + +.card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.card-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-gradient); + border-radius: 50%; + font-size: 1.5rem; +} + +/* ============================================ + BUTTONS + ============================================ */ +.btn { + font-family: var(--font-secondary); + font-size: 1.1rem; + font-weight: 600; + padding: var(--space-sm) var(--space-lg); + border: var(--border-thin); + border-radius: var(--border-radius-sm); + cursor: pointer; + transition: var(--transition-fast); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + text-decoration: none; + touch-action: manipulation; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.btn-primary { + background: var(--accent-primary); + color: white; + box-shadow: 3px 3px 0px var(--text-primary); +} + +.btn-primary:hover { + background: #ff5252; + transform: translate(-1px, -1px); + box-shadow: 4px 4px 0px var(--text-primary); +} + +.btn-primary:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 0px var(--text-primary); +} + +.btn-secondary { + background: var(--comic-blue); + color: var(--text-primary); + box-shadow: 3px 3px 0px var(--text-primary); +} + +.btn-secondary:hover { + background: #45b8b0; + transform: translate(-1px, -1px); +} + +.btn-success { + background: var(--comic-green); + color: white; + box-shadow: 3px 3px 0px var(--text-primary); +} + +.btn-warning { + background: var(--comic-orange); + color: white; + box-shadow: 3px 3px 0px var(--text-primary); +} + +.btn-danger { + background: var(--comic-red); + color: white; + box-shadow: 3px 3px 0px var(--text-primary); +} + +.btn-outline { + background: transparent; + color: var(--text-primary); + border: var(--border-thin); +} + +.btn-outline:hover { + background: var(--bg-tertiary); +} + +.btn-sm { + font-size: 0.9rem; + padding: var(--space-xs) var(--space-md); +} + +.btn-lg { + font-size: 1.25rem; + padding: var(--space-md) var(--space-xl); +} + +.btn-block { + width: 100%; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; +} + +/* ============================================ + FORM ELEMENTS + ============================================ */ +.form-group { + margin-bottom: var(--space-md); +} + +.form-label { + display: block; + font-family: var(--font-secondary); + font-size: 1rem; + font-weight: 600; + margin-bottom: var(--space-xs); + color: var(--text-primary); +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: var(--space-sm) var(--space-md); + font-family: var(--font-body); + font-size: 1rem; + background: var(--bg-secondary); + color: var(--text-primary); + border: var(--border-thin); + border-radius: var(--border-radius-sm); + box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1); + transition: var(--transition-fast); + touch-action: manipulation; +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1), 0 0 0 3px rgba(255, 107, 107, 0.3); +} + +.form-input::placeholder { + color: var(--text-muted); +} + +.form-error { + color: var(--comic-red); + font-size: 0.875rem; + margin-top: var(--space-xs); + display: flex; + align-items: center; + gap: var(--space-xs); +} + +.form-help { + color: var(--text-muted); + font-size: 0.875rem; + margin-top: var(--space-xs); +} + +/* ============================================ + NAVIGATION + ============================================ */ +.navbar { + background: var(--bg-secondary); + border-bottom: var(--border-thick); + padding: var(--space-md) var(--space-lg); + position: sticky; + top: 0; + z-index: 100; + box-shadow: var(--shadow-md); +} + +.navbar-container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-md); +} + +.navbar-brand { + font-family: var(--font-primary); + font-size: 1.75rem; + color: var(--accent-primary); + display: flex; + align-items: center; + gap: var(--space-sm); + text-decoration: none; +} + +.navbar-brand:hover { + color: var(--accent-primary); +} + +.navbar-menu { + display: flex; + align-items: center; + gap: var(--space-lg); + list-style: none; +} + +.navbar-item { + font-family: var(--font-secondary); + font-size: 1.1rem; + color: var(--text-primary); + text-decoration: none; + padding: var(--space-sm) var(--space-md); + border-radius: var(--border-radius-sm); + transition: var(--transition-fast); +} + +.navbar-item:hover { + background: var(--bg-tertiary); + color: var(--accent-primary); +} + +.navbar-item.active { + background: var(--accent-gradient); + color: white; +} + +.navbar-toggle { + display: none; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + padding: var(--space-sm); + color: var(--text-primary); +} + +/* ============================================ + HERO SECTION + ============================================ */ +.hero { + min-height: 80vh; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-2xl); + text-align: center; + position: relative; + overflow: hidden; +} + +.hero::before { + content: '✈️'; + position: absolute; + font-size: 20rem; + opacity: 0.1; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation: float 6s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translate(-50%, -50%) rotate(-5deg); } + 50% { transform: translate(-50%, -60%) rotate(5deg); } +} + +.hero-content { + max-width: 800px; + z-index: 1; +} + +.hero-title { + font-size: 4rem; + background: var(--accent-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: var(--space-lg); + text-shadow: none; +} + +.hero-subtitle { + font-family: var(--font-secondary); + font-size: 1.5rem; + color: var(--text-secondary); + margin-bottom: var(--space-xl); +} + +.hero-cta { + display: flex; + gap: var(--space-md); + justify-content: center; + flex-wrap: wrap; +} + +/* ============================================ + STATS & METRICS + ============================================ */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-md); +} + +.stat-card { + background: var(--bg-secondary); + border: var(--border-thin); + border-radius: var(--border-radius); + padding: var(--space-lg); + text-align: center; + box-shadow: var(--shadow-sm); + transition: var(--transition-normal); +} + +.stat-card:hover { + transform: scale(1.02); +} + +.stat-icon { + font-size: 2.5rem; + margin-bottom: var(--space-sm); +} + +.stat-value { + font-family: var(--font-primary); + font-size: 2rem; + color: var(--accent-primary); +} + +.stat-label { + font-family: var(--font-secondary); + color: var(--text-secondary); + font-size: 1rem; +} + +/* ============================================ + TABLES + ============================================ */ +.table-container { + overflow-x: auto; + border-radius: var(--border-radius); + border: var(--border-thin); +} + +.table { + width: 100%; + border-collapse: collapse; + font-family: var(--font-secondary); +} + +.table th { + background: var(--bg-tertiary); + font-family: var(--font-primary); + font-size: 1rem; + text-align: left; + padding: var(--space-md); + border-bottom: var(--border-thick); +} + +.table td { + padding: var(--space-md); + border-bottom: 1px solid var(--bg-tertiary); +} + +.table tr:hover td { + background: var(--bg-primary); +} + +.table tr:last-child td { + border-bottom: none; +} + +/* ============================================ + MODAL + ============================================ */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: var(--transition-normal); + padding: var(--space-md); +} + +.modal-overlay.active { + opacity: 1; + visibility: visible; +} + +.modal { + background: var(--bg-secondary); + border: var(--border-thick); + border-radius: var(--border-radius); + max-width: 500px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + transform: scale(0.9); + transition: var(--transition-normal); +} + +.modal-overlay.active .modal { + transform: scale(1); +} + +.modal-header { + padding: var(--space-lg); + border-bottom: var(--border-thin); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-title { + font-family: var(--font-primary); + font-size: 1.5rem; +} + +.modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--text-primary); + padding: var(--space-xs); +} + +.modal-body { + padding: var(--space-lg); +} + +.modal-footer { + padding: var(--space-lg); + border-top: var(--border-thin); + display: flex; + gap: var(--space-md); + justify-content: flex-end; +} + +/* ============================================ + WORLD MAP + ============================================ */ +.map-container { + position: relative; + width: 100%; + height: 600px; + background: var(--bg-tertiary); + border: var(--border-thick); + border-radius: var(--border-radius); + overflow: hidden; +} + +.map-canvas { + width: 100%; + height: 100%; + cursor: grab; +} + +.map-canvas:active { + cursor: grabbing; +} + +.map-controls { + position: absolute; + bottom: var(--space-md); + right: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-xs); + z-index: 10; +} + +.map-btn { + width: 40px; + height: 40px; + border-radius: 50%; + border: var(--border-thin); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1.25rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-sm); + transition: var(--transition-fast); +} + +.map-btn:hover { + background: var(--accent-primary); + color: white; +} + +.map-tooltip { + position: absolute; + background: var(--bg-secondary); + border: var(--border-thick); + border-radius: var(--border-radius-sm); + padding: var(--space-sm) var(--space-md); + pointer-events: none; + z-index: 100; + opacity: 0; + transition: opacity var(--transition-fast); + max-width: 200px; + box-shadow: var(--shadow-md); +} + +.map-tooltip.visible { + opacity: 1; +} + +/* ============================================ + TOAST NOTIFICATIONS + ============================================ */ +.toast-container { + position: fixed; + bottom: var(--space-lg); + right: var(--space-lg); + z-index: 2000; + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.toast { + background: var(--bg-secondary); + border: var(--border-thin); + border-radius: var(--border-radius-sm); + padding: var(--space-md) var(--space-lg); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + gap: var(--space-sm); + animation: slideIn 0.3s ease; + min-width: 250px; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.toast-success { + border-left: 4px solid var(--comic-green); +} + +.toast-error { + border-left: 4px solid var(--comic-red); +} + +.toast-warning { + border-left: 4px solid var(--comic-orange); +} + +.toast-info { + border-left: 4px solid var(--comic-blue); +} + +/* ============================================ + UTILITIES + ============================================ */ +.text-center { text-align: center; } +.text-right { text-align: right; } +.text-left { text-align: left; } + +.text-primary { color: var(--accent-primary); } +.text-success { color: var(--comic-green); } +.text-warning { color: var(--comic-orange); } +.text-danger { color: var(--comic-red); } +.text-info { color: var(--comic-blue); } + +.bg-primary { background: var(--bg-secondary); } +.bg-tertiary { background: var(--bg-tertiary); } + +.mt-sm { margin-top: var(--space-sm); } +.mt-md { margin-top: var(--space-md); } +.mt-lg { margin-top: var(--space-lg); } +.mt-xl { margin-top: var(--space-xl); } + +.mb-sm { margin-bottom: var(--space-sm); } +.mb-md { margin-bottom: var(--space-md); } +.mb-lg { margin-bottom: var(--space-lg); } +.mb-xl { margin-bottom: var(--space-xl); } + +.p-sm { padding: var(--space-sm); } +.p-md { padding: var(--space-md); } +.p-lg { padding: var(--space-lg); } + +.d-flex { display: flex; } +.d-grid { display: grid; } +.d-none { display: none; } + +.gap-sm { gap: var(--space-sm); } +.gap-md { gap: var(--space-md); } +.gap-lg { gap: var(--space-lg); } + +.flex-wrap { flex-wrap: wrap; } +.flex-center { justify-content: center; align-items: center; } +.flex-between { justify-content: space-between; } + +/* ============================================ + RESPONSIVE + ============================================ */ +@media (max-width: 768px) { + html { + font-size: 14px; + } + + h1 { font-size: 2rem; } + h2 { font-size: 1.75rem; } + h3 { font-size: 1.25rem; } + + .hero { + min-height: 70vh; + padding: var(--space-lg); + } + + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.1rem; + } + + .hero-cta { + flex-direction: column; + } + + .navbar-menu { + position: fixed; + top: 0; + left: -100%; + width: 80%; + max-width: 300px; + height: 100vh; + background: var(--bg-secondary); + flex-direction: column; + padding: var(--space-2xl) var(--space-lg); + gap: var(--space-md); + transition: left var(--transition-normal); + box-shadow: var(--shadow-lg); + z-index: 200; + } + + .navbar-menu.active { + left: 0; + } + + .navbar-toggle { + display: block; + } + + .panel { + padding: var(--space-md); + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .map-container { + height: 400px; + } + + .modal { + max-width: 95%; + } + + .table { + font-size: 0.875rem; + } + + .table th, + .table td { + padding: var(--space-sm); + } +} + +@media (max-width: 480px) { + .stats-grid { + grid-template-columns: 1fr; + } + + .btn-lg { + font-size: 1.1rem; + padding: var(--space-md); + } +} + +/* ============================================ + DARK MODE TOGGLE + ============================================ */ +.theme-toggle { + background: none; + border: none; + font-size: 1.25rem; + cursor: pointer; + padding: var(--space-sm); + border-radius: 50%; + transition: var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.theme-toggle:hover { + background: var(--bg-tertiary); +} + +/* ============================================ + LOADING STATES + ============================================ */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid var(--bg-tertiary); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.skeleton { + background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-secondary) 50%, var(--bg-tertiary) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--border-radius-sm); +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ============================================ + COMIC DECORATIONS + ============================================ */ +.comic-bubble { + background: white; + border: 3px solid var(--text-primary); + border-radius: 20px; + padding: var(--space-lg); + position: relative; + box-shadow: 4px 4px 0 var(--text-primary); +} + +.comic-bubble::after { + content: ''; + position: absolute; + bottom: -15px; + left: 30px; + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-top: 15px solid var(--text-primary); +} + +.comic-bubble::before { + content: ''; + position: absolute; + bottom: -10px; + left: 33px; + width: 0; + height: 0; + border-left: 12px solid transparent; + border-right: 12px solid transparent; + border-top: 12px solid white; + z-index: 1; +} + +/* Badge */ +.badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + font-size: 0.75rem; + font-weight: 600; + border-radius: 12px; + background: var(--accent-gradient); + color: white; +} + +.badge-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.badge-success { + background: var(--comic-green); +} + +.badge-warning { + background: var(--comic-orange); +} + +.badge-danger { + background: var(--comic-red); +} + +/* Progress Bar */ +.progress { + height: 24px; + background: var(--bg-tertiary); + border-radius: 12px; + border: 2px solid var(--text-primary); + overflow: hidden; + box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1); +} + +.progress-bar { + height: 100%; + background: var(--accent-gradient); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-family: var(--font-secondary); + font-size: 0.875rem; + transition: width var(--transition-normal); +} + +/* Touch-Friendly Interactive Elements */ +@media (hover: none) and (pointer: coarse) { + .btn, + .form-input, + .form-select, + .navbar-item { + min-height: 44px; + } + + .btn { + padding: var(--space-md) var(--space-lg); + } + + .map-btn { + width: 48px; + height: 48px; + } +} diff --git a/www/public/assets/js/main.js b/www/public/assets/js/main.js new file mode 100644 index 0000000..7492a66 --- /dev/null +++ b/www/public/assets/js/main.js @@ -0,0 +1,865 @@ +/** + * Airline Tycoon - Main JavaScript + * Vanilla JS, Mobile-responsive, Touch-friendly + */ + +(function() { + 'use strict'; + + // ============================================ + // GLOBAL STATE + // ============================================ + const App = { + config: { + basePath: window.BASE_PATH || '/', + apiTimeout: 10000 + }, + state: { + theme: localStorage.getItem('theme') || 'light', + map: null, + markers: [], + selectedAirport: null + } + }; + + // ============================================ + // INITIALIZATION + // ============================================ + document.addEventListener('DOMContentLoaded', function() { + initTheme(); + initNavigation(); + initModals(); + initToasts(); + initForms(); + initWorldMap(); + initTouchHandlers(); + }); + + // ============================================ + // THEME MANAGEMENT + // ============================================ + function initTheme() { + const themeToggle = document.getElementById('themeToggle'); + + // System-Präferenz prüfen + if (!localStorage.getItem('theme')) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + App.state.theme = prefersDark ? 'dark' : 'light'; + } + + applyTheme(App.state.theme); + + if (themeToggle) { + themeToggle.addEventListener('click', toggleTheme); + } + } + + function toggleTheme() { + App.state.theme = App.state.theme === 'light' ? 'dark' : 'light'; + localStorage.setItem('theme', App.state.theme); + applyTheme(App.state.theme); + } + + function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + + const themeIcon = document.querySelector('#themeToggle, .theme-toggle'); + if (themeIcon) { + themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙'; + } + } + + // ============================================ + // NAVIGATION + // ============================================ + function initNavigation() { + const navbarToggle = document.querySelector('.navbar-toggle'); + const navbarMenu = document.querySelector('.navbar-menu'); + const navbarOverlay = document.querySelector('.navbar-overlay'); + + if (navbarToggle && navbarMenu) { + navbarToggle.addEventListener('click', function() { + navbarMenu.classList.toggle('active'); + navbarOverlay?.classList.toggle('active'); + }); + + navbarOverlay?.addEventListener('click', function() { + navbarMenu.classList.remove('active'); + navbarOverlay?.classList.remove('active'); + }); + } + + // Aktive Navigation highlight + const currentPath = window.location.pathname; + document.querySelectorAll('.navbar-item').forEach(item => { + if (item.getAttribute('href') === currentPath) { + item.classList.add('active'); + } + }); + } + + // ============================================ + // MODALS + // ============================================ + function initModals() { + document.querySelectorAll('[data-modal]').forEach(trigger => { + trigger.addEventListener('click', function(e) { + e.preventDefault(); + const modalId = this.getAttribute('data-modal'); + openModal(modalId); + }); + }); + + document.querySelectorAll('.modal-overlay').forEach(overlay => { + overlay.addEventListener('click', function(e) { + if (e.target === this) { + closeModal(this.id); + } + }); + }); + + document.querySelectorAll('.modal-close').forEach(btn => { + btn.addEventListener('click', function() { + const modal = this.closest('.modal-overlay'); + if (modal) { + closeModal(modal.id); + } + }); + }); + + // ESC Key zum Schließen + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + document.querySelectorAll('.modal-overlay.active').forEach(modal => { + closeModal(modal.id); + }); + } + }); + } + + function openModal(id) { + const modal = document.getElementById(id); + if (modal) { + modal.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + } + + function closeModal(id) { + const modal = document.getElementById(id); + if (modal) { + modal.classList.remove('active'); + document.body.style.overflow = ''; + } + } + + // ============================================ + // TOAST NOTIFICATIONS + // ============================================ + function initToasts() { + // Toasts werden dynamisch erstellt + } + + window.showToast = function(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer') || createToastContainer(); + + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.innerHTML = ` + ${getToastIcon(type)} + ${escapeHtml(message)} + `; + + container.appendChild(toast); + + setTimeout(() => { + toast.style.animation = 'slideIn 0.3s ease reverse'; + setTimeout(() => toast.remove(), 300); + }, duration); + }; + + function createToastContainer() { + const container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container'; + document.body.appendChild(container); + return container; + } + + function getToastIcon(type) { + const icons = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️' + }; + return icons[type] || icons.info; + } + + // ============================================ + // FORM HANDLING + // ============================================ + function initForms() { + document.querySelectorAll('form[data-ajax]').forEach(form => { + form.addEventListener('submit', handleAjaxForm); + }); + + document.querySelectorAll('form').forEach(form => { + // CSRF Token hinzufügen + const csrfInput = form.querySelector('input[name="csrf_token"]'); + if (!csrfInput && form.dataset.csrf !== 'false') { + const token = document.querySelector('meta[name="csrf-token"]'); + if (token) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'csrf_token'; + input.value = token.content; + form.appendChild(input); + } + } + + // Form Validation + form.addEventListener('submit', function(e) { + if (!validateForm(this)) { + e.preventDefault(); + } + }); + }); + } + + async function handleAjaxForm(e) { + e.preventDefault(); + + const form = e.target; + const submitBtn = form.querySelector('[type="submit"]'); + const originalText = submitBtn?.textContent; + + try { + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = ''; + } + + const formData = new FormData(form); + const response = await fetch(form.action || window.location.href, { + method: form.method || 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + const data = await response.json(); + + if (response.ok) { + showToast(data.message || 'Erfolgreich!', 'success'); + + if (data.redirect) { + setTimeout(() => { + window.location.href = data.redirect; + }, 1000); + } else if (data.reset && form.dataset.reset !== 'false') { + form.reset(); + } + + // Callback + if (typeof window[data.onSuccess] === 'function') { + window[data.onSuccess](data); + } + } else { + showToast(data.error || 'Ein Fehler ist aufgetreten', 'error'); + } + } catch (error) { + console.error('Form submission error:', error); + showToast('Netzwerkfehler. Bitte versuche es erneut.', 'error'); + } finally { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = originalText; + } + } + } + + function validateForm(form) { + let isValid = true; + + form.querySelectorAll('[required]').forEach(field => { + if (!field.value.trim()) { + isValid = false; + showFieldError(field, 'Dieses Feld ist erforderlich'); + } else { + clearFieldError(field); + } + }); + + // Email Validation + form.querySelectorAll('[type="email"]').forEach(field => { + if (field.value && !isValidEmail(field.value)) { + isValid = false; + showFieldError(field, 'Bitte gib eine gültige E-Mail ein'); + } + }); + + return isValid; + } + + function showFieldError(field, message) { + clearFieldError(field); + + field.classList.add('error'); + + const error = document.createElement('div'); + error.className = 'form-error'; + error.textContent = message; + + field.parentNode.appendChild(error); + } + + function clearFieldError(field) { + field.classList.remove('error'); + + const error = field.parentNode.querySelector('.form-error'); + if (error) { + error.remove(); + } + } + + function isValidEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + } + + // ============================================ + // WORLD MAP + // ============================================ + function initWorldMap() { + const mapContainer = document.getElementById('mapContainer'); + if (!mapContainer) return; + + // Map Canvas erstellen + const canvas = document.createElement('canvas'); + canvas.id = 'mapCanvas'; + canvas.className = 'map-canvas'; + mapContainer.appendChild(canvas); + + // Controls erstellen + const controls = document.createElement('div'); + controls.className = 'map-controls'; + controls.innerHTML = ` + + + + `; + mapContainer.appendChild(controls); + + // Tooltip erstellen + const tooltip = document.createElement('div'); + tooltip.className = 'map-tooltip'; + tooltip.id = 'mapTooltip'; + mapContainer.appendChild(tooltip); + + // Map State + App.state.map = { + canvas: canvas, + ctx: canvas.getContext('2d'), + zoom: 1, + offsetX: 0, + offsetY: 0, + isDragging: false, + lastX: 0, + lastY: 0 + }; + + // Canvas Größe setzen + resizeMapCanvas(); + window.addEventListener('resize', resizeMapCanvas); + + // Map Events + canvas.addEventListener('mousedown', mapMouseDown); + canvas.addEventListener('mousemove', mapMouseMove); + canvas.addEventListener('mouseup', mapMouseUp); + canvas.addEventListener('mouseleave', mapMouseUp); + canvas.addEventListener('wheel', mapWheel); + + // Touch Events + canvas.addEventListener('touchstart', mapTouchStart, { passive: false }); + canvas.addEventListener('touchmove', mapTouchMove, { passive: false }); + canvas.addEventListener('touchend', mapTouchEnd); + + // Control Events + document.getElementById('mapZoomIn')?.addEventListener('click', () => mapZoom(1.2)); + document.getElementById('mapZoomOut')?.addEventListener('click', () => mapZoom(0.8)); + document.getElementById('mapReset')?.addEventListener('click', mapReset); + + // Map laden + loadMapData(); + } + + function resizeMapCanvas() { + if (!App.state.map) return; + + const rect = App.state.map.canvas.parentElement.getBoundingClientRect(); + App.state.map.canvas.width = rect.width; + App.state.map.canvas.height = rect.height; + renderMap(); + } + + async function loadMapData() { + try { + const response = await fetch(App.config.basePath + 'api/airports.php?action=list'); + const data = await response.json(); + + if (data.airports) { + App.state.airports = data.airports; + renderMap(); + } + } catch (error) { + console.error('Map data loading error:', error); + } + } + + function renderMap() { + if (!App.state.map || !App.state.map.ctx) return; + + const { canvas, ctx, zoom, offsetX, offsetY } = App.state.map; + + // Canvas löschen + ctx.fillStyle = getComputedStyle(document.documentElement) + .getPropertyValue('--bg-tertiary').trim() || '#e8e0d4'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Weltkarte zeichnen (vereinfacht) + drawWorldMap(ctx, canvas.width, canvas.height, zoom, offsetX, offsetY); + + // Flughäfen zeichnen + if (App.state.airports) { + App.state.airports.forEach(airport => { + drawAirport(ctx, airport, zoom, offsetX, offsetY); + }); + } + } + + function drawWorldMap(ctx, width, height, zoom, offsetX, offsetY) { + // Vereinfachte Weltkarte mit Kontinenten + ctx.save(); + ctx.translate(width / 2 + offsetX, height / 2 + offsetY); + ctx.scale(zoom, zoom); + + // Ozean Hintergrund + ctx.fillStyle = 'rgba(78, 205, 196, 0.1)'; + ctx.beginPath(); + ctx.ellipse(0, 0, 800, 400, 0, 0, Math.PI * 2); + ctx.fill(); + + // Vereinfachte Kontinente + const continents = [ + // Nordamerika + { x: -300, y: -100, points: [[0,0], [100,20], [150,80], [100,150], [0,120], [-50,50]], color: '#6BCB77' }, + // Südamerika + { x: -100, y: 100, points: [[0,0], [50,20], [60,80], [30,150], [-20,100], [-30,40]], color: '#6BCB77' }, + // Europa + { x: 100, y: -80, points: [[0,0], [80,10], [100,50], [60,80], [10,60], [-20,20]], color: '#FFD93D' }, + // Afrika + { x: 120, y: 60, points: [[0,0], [70,10], [90,60], [50,120], [0,80], [-30,30]], color: '#FFD93D' }, + // Asien + { x: 350, y: -50, points: [[0,0], [150,20], [200,80], [180,150], [100,100], [30,30]], color: '#FF9F43' }, + // Australien + { x: 450, y: 150, points: [[0,0], [80,20], [90,70], [50,100], [-10,60], [-30,20]], color: '#FF6B6B' } + ]; + + continents.forEach(continent => { + ctx.fillStyle = continent.color + '40'; + ctx.strokeStyle = continent.color; + ctx.lineWidth = 2 / zoom; + + ctx.beginPath(); + ctx.moveTo(continent.x + continent.points[0][0], continent.y + continent.points[0][1]); + continent.points.forEach((point, i) => { + if (i > 0) ctx.lineTo(continent.x + point[0], continent.y + point[1]); + }); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + }); + + ctx.restore(); + } + + function drawAirport(ctx, airport, zoom, offsetX, offsetY) { + const { canvas } = App.state.map; + + // Koordinaten zu Pixeln konvertieren + const x = canvas.width / 2 + offsetX + (airport.longitude * 8 * zoom); + const y = canvas.height / 2 + offsetY + (airport.latitude * -5 * zoom); + + // Sichtbarkeit prüfen + if (x < -20 || x > canvas.width + 20 || y < -20 || y > canvas.height + 20) return; + + ctx.save(); + + // Airport Marker + const isSelected = App.state.selectedAirport?.id === airport.id; + const radius = (isSelected ? 12 : 8) / zoom; + + // Glow für ausgewählten Flughafen + if (isSelected) { + ctx.fillStyle = 'rgba(255, 107, 107, 0.3)'; + ctx.beginPath(); + ctx.arc(x, y, radius * 2, 0, Math.PI * 2); + ctx.fill(); + } + + // Hauptmarker + ctx.fillStyle = isSelected ? '#FF6B6B' : '#4ECDC4'; + ctx.strokeStyle = '#2c2416'; + ctx.lineWidth = 2 / zoom; + + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + // IATA Code + ctx.fillStyle = '#2c2416'; + ctx.font = `${10 / zoom}px Nunito, sans-serif`; + ctx.textAlign = 'center'; + ctx.fillText(airport.iata_code, x, y + radius + 14 / zoom); + + ctx.restore(); + + // Airport in Marker-Array speichern + airport.pixelX = x; + airport.pixelY = y; + } + + function getAirportFromPixel(x, y) { + if (!App.state.airports) return null; + + return App.state.airports.find(airport => { + const dx = Math.abs(airport.pixelX - x); + const dy = Math.abs(airport.pixelY - y); + return dx < 20 && dy < 20; + }); + } + + function mapMouseDown(e) { + const rect = App.state.map.canvas.getBoundingClientRect(); + App.state.map.isDragging = true; + App.state.map.lastX = e.clientX - rect.left; + App.state.map.lastY = e.clientY - rect.top; + App.state.map.dragStartTime = Date.now(); + App.state.map.dragStartX = App.state.map.offsetX; + App.state.map.dragStartY = App.state.map.offsetY; + } + + function mapMouseMove(e) { + const rect = App.state.map.canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (App.state.map.isDragging) { + const dx = x - App.state.map.lastX; + const dy = y - App.state.map.lastY; + + App.state.map.offsetX += dx; + App.state.map.offsetY += dy; + + App.state.map.lastX = x; + App.state.map.lastY = y; + + renderMap(); + } else { + // Tooltip bei Hover + const airport = getAirportFromPixel(x, y); + const tooltip = document.getElementById('mapTooltip'); + + if (airport) { + tooltip.innerHTML = ` + ${airport.iata_code} - ${airport.name}
+ ${airport.city}, ${airport.country}
+ Passagiere: ${formatNumber(airport.passenger_volume)}/Jahr + `; + tooltip.style.left = (x + 15) + 'px'; + tooltip.style.top = (y - 10) + 'px'; + tooltip.classList.add('visible'); + App.state.map.canvas.style.cursor = 'pointer'; + } else { + tooltip.classList.remove('visible'); + App.state.map.canvas.style.cursor = 'grab'; + } + } + } + + function mapMouseUp(e) { + if (App.state.map.isDragging) { + const dragDuration = Date.now() - App.state.map.dragStartTime; + const dragDistance = Math.abs(App.state.map.offsetX - App.state.map.dragStartX) + + Math.abs(App.state.map.offsetY - App.state.map.dragStartY); + + // Klick erkennen (kurze Dauer, little movement) + if (dragDuration < 200 && dragDistance < 5) { + const rect = App.state.map.canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const airport = getAirportFromPixel(x, y); + + if (airport) { + selectAirport(airport); + } + } + } + + App.state.map.isDragging = false; + } + + function mapWheel(e) { + e.preventDefault(); + const delta = e.deltaY > 0 ? 0.9 : 1.1; + mapZoom(delta); + } + + function mapTouchStart(e) { + e.preventDefault(); + + if (e.touches.length === 1) { + const touch = e.touches[0]; + const rect = App.state.map.canvas.getBoundingClientRect(); + + App.state.map.isDragging = true; + App.state.map.lastX = touch.clientX - rect.left; + App.state.map.lastY = touch.clientY - rect.top; + App.state.map.dragStartTime = Date.now(); + App.state.map.dragStartX = App.state.map.offsetX; + App.state.map.dragStartY = App.state.map.offsetY; + App.state.map.touchStartX = touch.clientX; + App.state.map.touchStartY = touch.clientY; + } + } + + function mapTouchMove(e) { + e.preventDefault(); + + if (e.touches.length === 1 && App.state.map.isDragging) { + const touch = e.touches[0]; + const rect = App.state.map.canvas.getBoundingClientRect(); + const x = touch.clientX - rect.left; + const y = touch.clientY - rect.top; + + const dx = x - App.state.map.lastX; + const dy = y - App.state.map.lastY; + + App.state.map.offsetX += dx; + App.state.map.offsetY += dy; + + App.state.map.lastX = x; + App.state.map.lastY = y; + + renderMap(); + } + } + + function mapTouchEnd(e) { + if (App.state.map.isDragging) { + const touch = e.changedTouches[0]; + const dragDuration = Date.now() - App.state.map.dragStartTime; + const dragDistance = Math.abs(touch.clientX - App.state.map.touchStartX) + + Math.abs(touch.clientY - App.state.map.touchStartY); + + // Tap erkennen + if (dragDuration < 200 && dragDistance < 10) { + const rect = App.state.map.canvas.getBoundingClientRect(); + const x = touch.clientX - rect.left; + const y = touch.clientY - rect.top; + const airport = getAirportFromPixel(x, y); + + if (airport) { + selectAirport(airport); + } + } + } + + App.state.map.isDragging = false; + } + + function mapZoom(factor) { + const newZoom = Math.max(0.5, Math.min(4, App.state.map.zoom * factor)); + + // Zoom um Mausposition + const rect = App.state.map.canvas.getBoundingClientRect(); + const mouseX = App.state.map.lastX - rect.width / 2; + const mouseY = App.state.map.lastY - rect.height / 2; + + App.state.map.offsetX = mouseX - (mouseX - App.state.map.offsetX) * factor; + App.state.map.offsetY = mouseY - (mouseY - App.state.map.offsetY) * factor; + App.state.map.zoom = newZoom; + + renderMap(); + } + + function mapReset() { + App.state.map.zoom = 1; + App.state.map.offsetX = 0; + App.state.map.offsetY = 0; + renderMap(); + } + + function selectAirport(airport) { + App.state.selectedAirport = airport; + renderMap(); + + // Airport Info Panel aktualisieren + const infoPanel = document.getElementById('airportInfo'); + if (infoPanel && airport) { + infoPanel.innerHTML = ` +

${airport.iata_code} - ${airport.name}

+

Stadt: ${airport.city}, ${airport.country}

+

Kontinent: ${airport.continent}

+

Passagiere: ${formatNumber(airport.passenger_volume)}/Jahr

+

Nachfrage: ${(airport.demand_multiplier * 100).toFixed(0)}%

+ + `; + infoPanel.classList.add('active'); + } + + // Event emit + document.dispatchEvent(new CustomEvent('airportSelected', { detail: airport })); + } + + // ============================================ + // TOUCH HANDLERS + // ============================================ + function initTouchHandlers() { + // Touch-freundliche Button-Press-Effekte + document.querySelectorAll('.btn').forEach(btn => { + btn.addEventListener('touchstart', function() { + this.classList.add('pressed'); + }, { passive: true }); + + btn.addEventListener('touchend', function() { + this.classList.remove('pressed'); + }, { passive: true }); + }); + } + + // ============================================ + // API HELPERS + // ============================================ + window.api = { + async get(endpoint, params = {}) { + const queryString = new URLSearchParams(params).toString(); + const url = App.config.basePath + 'api/' + endpoint + (queryString ? '?' + queryString : ''); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + return response.json(); + }, + + async post(endpoint, data = {}) { + const formData = new FormData(); + Object.keys(data).forEach(key => formData.append(key, data[key])); + + const response = await fetch(App.config.basePath + 'api/' + endpoint, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + return response.json(); + } + }; + + // ============================================ + // GAME ACTIONS + // ============================================ + window.createRouteFromAirport = async function(fromAirportId) { + const modal = document.getElementById('createRouteModal'); + if (modal) { + document.getElementById('routeFromAirport').value = fromAirportId; + openModal('createRouteModal'); + } + }; + + window.executeFlight = async function(routeId, aircraftId) { + try { + const result = await api.post('game.php?action=flight', { + route_id: routeId, + aircraft_id: aircraftId + }); + + if (result.success) { + showToast(`Flug erfolgreich! +${formatMoney(result.profit)}`, 'success'); + + // Dashboard aktualisieren + if (typeof updateDashboard === 'function') { + updateDashboard(); + } + } else { + showToast(result.error || 'Flug fehlgeschlagen', 'error'); + } + } catch (error) { + showToast('Netzwerkfehler', 'error'); + } + }; + + window.purchaseAircraft = async function(type) { + try { + const result = await api.post('game.php?action=purchase', { + aircraft_type: type + }); + + if (result.success) { + showToast(`${type} gekauft für ${formatMoney(result.price)}`, 'success'); + + // Modal schließen + const modal = document.getElementById('purchaseAircraftModal'); + if (modal) closeModal('purchaseAircraftModal'); + + // Dashboard aktualisieren + if (typeof updateDashboard === 'function') { + updateDashboard(); + } + } else { + showToast(result.error || 'Kauf fehlgeschlagen', 'error'); + } + } catch (error) { + showToast('Netzwerkfehler', 'error'); + } + }; + + // ============================================ + // UTILITIES + // ============================================ + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + function formatNumber(num) { + return new Intl.NumberFormat('de-DE').format(num); + } + + function formatMoney(amount) { + return new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR' + }).format(amount); + } + + // ============================================ + // EXPORTS + // ============================================ + window.App = App; + window.showToast = showToast; + window.openModal = openModal; + window.closeModal = closeModal; + +})(); diff --git a/www/public/dashboard.php b/www/public/dashboard.php new file mode 100644 index 0000000..6de0944 --- /dev/null +++ b/www/public/dashboard.php @@ -0,0 +1,461 @@ +getUserById($userId); + +if (!$user) { + Session::logout(); + redirect('/login.php'); +} + +Session::refreshUserData($db); + +// Aktualisiere User-Daten +$user = $db->getUserById($userId); +$stats = $db->getUserStats($userId); +$todayStats = $db->getTodayStats($userId); +$aircraft = $db->getUserAircraft($userId); +$routes = $db->getUserRoutes($userId); + +// Level-Fortschritt berechnen +$expForNextLevel = $config['game']['level_up_threshold'] * pow($config['game']['level_multiplier'], $user['level'] - 1); +$levelProgress = ($user['experience'] / $expForNextLevel) * 100; +?> + + + + + + + + Dashboard - Airline Tycoon + + + + + + + +
+ +
+

👋 Willkommen, !

+

+ Level | +

+ + +
+
+ Erfahrung + / XP +
+
+
+ % +
+
+
+
+ + +
+
+
💰
+
+
Kontostand
+
+ +
+
✈️
+
+
Flugzeuge
+
+ +
+
🛫
+
+
Routen
+
+ +
+
👥
+
+
Passagiere (heute)
+
+ +
+
📈
+
+
Gewinn (heute)
+
+ +
+
🏆
+
+
Flüge (gesamt)
+
+
+ + +
+

⚡ Schnellaktionen

+ + +
+ + +
+ +
+

✈️ Meine Flotte

+ + +
+

Du hast noch keine Flugzeuge.

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
NameTypZustandFlügeAktion
+ + % + + + + + + Keine Route + +
+
+ +
+ + +
+

🛫 Meine Routen

+ + +
+

Du hast noch keine Routen.

+ + 🛫 Jetzt Route erstellen + +
+ +
+ + + + + + + + + + + + + + + + + + + +
VonNachDistanzNachfrage
+ +
+
+ +
+
km +
+
+ % +
+
+
+
+ +
+
+ + +
+

📜 Letzte Flüge

+ + select( + "SELECT f.*, a1.iata_code as from_iata, a2.iata_code as to_iata, + ac.name as aircraft_name, ac.aircraft_type + FROM flights f + JOIN routes r ON f.route_id = r.id + JOIN airports a1 ON r.from_airport_id = a1.id + JOIN airports a2 ON r.to_airport_id = a2.id + JOIN aircraft ac ON f.aircraft_id = ac.id + WHERE f.user_id = ? + ORDER BY f.created_at DESC + LIMIT 10", + [$userId] + ); + ?> + + +

Noch keine Flüge durchgeführt.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
FlugFlugzeugPassagiereUmsatzGewinnZeit
+ → + + + +
+
+ +
+
+ + + + + + + + +
+ + + + + diff --git a/www/public/index.php b/www/public/index.php new file mode 100644 index 0000000..36ea6e9 --- /dev/null +++ b/www/public/index.php @@ -0,0 +1,203 @@ + + + + + + + + + Airline Tycoon - Werde zum Flugmagnat! + + + + + + + + +
+
+

Werde zum Flugmagnat!

+

+ Baue deine eigene Airline auf, verbinde Städte auf der ganzen Welt + und werde zur Nummer 1 in diesem kostenlosen Browser-MMO! +

+ + + +
+

+ 💡 Tipp: Starte mit kurzen Strecken, spare Geld und kaufe größere Flugzeuge, + um noch mehr Passagiere zu transportieren! +

+
+
+
+ + +
+
+

🎮 Spielfeatures

+ +
+
+
🛫
+

60+ Flughäfen

+

Verbinde Städte auf allen Kontinenten und erweitere dein Streckennetz!

+
+ +
+
✈️
+

5 Flugzeugtypen

+

Vom kleinen Dash 8 bis zum riesigen Airbus A380 - wähle das richtige Flugzeug!

+
+ +
+
💰
+

Wirtschaftssimulation

+

Verwalte dein Budget, maximiere Gewinne und werde reich!

+
+ +
+
🏆
+

Rangliste

+

Konkurriere mit anderen Spielern um den Spitzenplatz!

+
+ +
+
📱
+

Mobile optimiert

+

Spiele überall - am Computer, Tablet oder Smartphone!

+
+ +
+
🌙
+

Dark Mode

+

Wähle zwischen hellem und dunklem Design - wie es dir gefällt!

+
+
+
+
+ + +
+
+

🖥️ Spielansichten

+ +
+
+

📊 Dashboard

+

Behalte deine Finanzen, Flugzeuge und Routes im Blick!

+
+ +
+

🌍 Weltkarte

+

Interaktive Karte mit allen Flughäfen. Tippe zum Auswählen!

+
+ +
+

✈️ Flottenverwaltung

+

Verwalte deine Flugzeuge und behalte deren Zustand im Auge!

+
+
+
+
+ + +
+
+

🎯 So spielst du

+ +
+
+
1
+

Registrieren

+

Kostenloses Konto erstellen

+
+ +
+
2
+

Flugzeuge kaufen

+

Starte mit deinem ersten Flieger

+
+ +
+
3
+

Routen erstellen

+

Verbinde Flughäfen weltweit

+
+ +
+
4
+

Fliegen!

+

Verdiene Geld und Level auf

+
+
+
+
+ + +
+
+

Bereit abzuheben?

+

+ Starte jetzt dein Flugabenteuer! Keine Kosten, keine Downloads - + einfach Browser öffnen und spielen! +

+ + 🚀 Jetzt kostenlos spielen! + +
+
+ + + + + +
+ + + + diff --git a/www/public/login.php b/www/public/login.php new file mode 100644 index 0000000..65d0fdc --- /dev/null +++ b/www/public/login.php @@ -0,0 +1,134 @@ +getUserByUsername($username); + + if ($user && password_verify($password, $user['password_hash'])) { + Session::login($user); + + // Redirect merken + $redirect = $_GET['redirect'] ?? '/dashboard.php'; + redirect($redirect); + } else { + $error = 'Benutzername oder Passwort falsch.'; + } + } +} + +// Redirect wenn bereits eingeloggt +if (Session::isLoggedIn()) { + redirect('/dashboard.php'); +} +?> + + + + + + Anmelden - Airline Tycoon + + + + + + + + +
+
+

🔐 Anmelden

+ + +
+

+
+ + + +
+

+
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+

Noch kein Konto?

+ + Jetzt registrieren + +
+
+
+ + +
+ + + + diff --git a/www/public/logout.php b/www/public/logout.php new file mode 100644 index 0000000..f9cdbf9 --- /dev/null +++ b/www/public/logout.php @@ -0,0 +1,11 @@ + 50) { + $error = 'Der Benutzername muss zwischen 3 und 50 Zeichen haben.'; + } elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) { + $error = 'Der Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten.'; + } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $error = 'Bitte gib eine gültige E-Mail-Adresse ein.'; + } elseif (strlen($password) < 8) { + $error = 'Das Passwort muss mindestens 8 Zeichen haben.'; + } elseif ($password !== $password_confirm) { + $error = 'Die Passwörter stimmen nicht überein.'; + } else { + // Prüfen ob Benutzer bereits existiert + if ($db->getUserByUsername($username)) { + $error = 'Dieser Benutzername ist bereits vergeben.'; + } elseif ($db->getUserByEmail($email)) { + $error = 'Diese E-Mail-Adresse wird bereits verwendet.'; + } else { + // Benutzer erstellen + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + $userId = $db->createUser($username, $email, $passwordHash); + + // Airline Name setzen falls angegeben + if (!empty($airline_name)) { + $db->update('users', ['airline_name' => $airline_name], 'id = ?', [$userId]); + } else { + $db->update('users', ['airline_name' => $username . "'s Airlines"], 'id = ?', [$userId]); + } + + // Airports aus CSV laden falls noch nicht vorhanden + $airportCount = $db->select("SELECT COUNT(*) as cnt FROM airports")[0]['cnt'] ?? 0; + if ($airportCount == 0) { + $db->loadAirportsFromCsv($config['paths']['data'] . '/airports.csv'); + } + + Session::success('Registrierung erfolgreich! Du kannst dich jetzt anmelden.'); + redirect('/login.php'); + } + } +} + +// Redirect wenn bereits eingeloggt +if (Session::isLoggedIn()) { + redirect('/dashboard.php'); +} +?> + + + + + + Registrieren - Airline Tycoon + + + + + + + + +
+
+

📝 Registrieren

+ + +
+

+
+ + +
+ + +
+ + +

3-50 Zeichen, nur Buchstaben, Zahlen und Unterstriche

+
+ +
+ + +
+ +
+ + +

Leer lassen für Standard-Name

+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+

Bereits ein Konto?

+ + Jetzt anmelden + +
+ + +
+

+ 🎁 Willkommensbonus: Starte mit 500.000 € und einem Dash 8-100 Flugzeug! +

+
+
+
+ + +
+ + + + diff --git a/www/src/Database.php b/www/src/Database.php new file mode 100644 index 0000000..9d170c6 --- /dev/null +++ b/www/src/Database.php @@ -0,0 +1,373 @@ +config = require dirname(__DIR__) . '/src/config.php'; + $this->connect(); + } + + public static function getInstance(): Database { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + private function connect(): void { + $dbPath = $this->config['database']['path']; + $dbDir = dirname($dbPath); + + // Verzeichnis erstellen falls nicht vorhanden + if (!is_dir($dbDir)) { + mkdir($dbDir, 0755, true); + } + + // SQLite Datenbank erstellen/öffnen + $this->connection = new PDO('sqlite:' . $dbPath); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->connection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + + // Schema initialisieren + $this->initializeSchema(); + } + + private function initializeSchema(): void { + $schemaPath = $this->config['database']['schema']; + + if (file_exists($schemaPath)) { + $schema = file_get_contents($schemaPath); + $this->connection->exec($schema); + } + } + + public function getConnection(): PDO { + return $this->connection; + } + + // Generic CRUD Operations + public function insert(string $table, array $data): int { + $columns = implode(', ', array_keys($data)); + $placeholders = implode(', ', array_fill(0, count($data), '?')); + + $sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})"; + $stmt = $this->connection->prepare($sql); + $stmt->execute(array_values($data)); + + return (int) $this->connection->lastInsertId(); + } + + public function update(string $table, array $data, string $where, array $whereValues = []): int { + $set = implode(' = ?, ', array_keys($data)) . ' = ?'; + + $sql = "UPDATE {$table} SET {$set} WHERE {$where}"; + $stmt = $this->connection->prepare($sql); + $stmt->execute(array_merge(array_values($data), $whereValues)); + + return $stmt->rowCount(); + } + + public function delete(string $table, string $where, array $whereValues = []): int { + $sql = "DELETE FROM {$table} WHERE {$where}"; + $stmt = $this->connection->prepare($sql); + $stmt->execute($whereValues); + + return $stmt->rowCount(); + } + + public function select(string $sql, array $params = []): array { + $stmt = $this->connection->prepare($sql); + $stmt->execute($params); + return $stmt->fetchAll(); + } + + public function selectOne(string $sql, array $params = []): ?array { + $stmt = $this->connection->prepare($sql); + $stmt->execute($params); + $result = $stmt->fetch(); + return $result ?: null; + } + + // User-spezifische Methoden + public function getUserById(int $id): ?array { + return $this->selectOne( + "SELECT id, username, email, airline_name, balance, level, experience, created_at, last_login + FROM users WHERE id = ? AND is_active = 1", + [$id] + ); + } + + public function getUserByUsername(string $username): ?array { + return $this->selectOne( + "SELECT * FROM users WHERE username = ? AND is_active = 1", + [$username] + ); + } + + public function getUserByEmail(string $email): ?array { + return $this->selectOne( + "SELECT * FROM users WHERE email = ? AND is_active = 1", + [$email] + ); + } + + public function createUser(string $username, string $email, string $passwordHash): int { + return $this->insert('users', [ + 'username' => $username, + 'email' => $email, + 'password_hash' => $passwordHash, + 'balance' => $this->config['game']['start_balance'], + 'created_at' => date('Y-m-d H:i:s') + ]); + } + + // Airport Methoden + public function getAllAirports(): array { + return $this->select("SELECT * FROM airports ORDER BY country, city"); + } + + public function getAirportByIata(string $iata): ?array { + return $this->selectOne("SELECT * FROM airports WHERE iata_code = ?", [$iata]); + } + + public function loadAirportsFromCsv(string $csvPath): int { + if (!file_exists($csvPath)) { + return 0; + } + + $count = 0; + $handle = fopen($csvPath, 'r'); + + // Header überspringen + fgetcsv($handle, 0, ';'); + + while (($data = fgetcsv($handle, 0, ';')) !== false) { + if (count($data) >= 10) { + $this->insert('airports', [ + 'id' => (int) $data[0], + 'iata_code' => $data[1], + 'name' => $data[2], + 'city' => $data[3], + 'country' => $data[4], + 'continent' => $data[5], + 'latitude' => (float) str_replace(',', '.', $data[6]), + 'longitude' => (float) str_replace(',', '.', $data[7]), + 'timezone' => $data[8] ?? 'UTC', + 'passenger_volume' => (int) ($data[9] ?? 1000000), + 'demand_multiplier' => (float) ($data[10] ?? 1.00) + ]); + $count++; + } + } + + fclose($handle); + return $count; + } + + // Flugzeug Methoden + public function getUserAircraft(int $userId): array { + return $this->select( + "SELECT * FROM aircraft WHERE user_id = ? ORDER BY created_at DESC", + [$userId] + ); + } + + public function purchaseAircraft(int $userId, string $type, string $name, float $price): int { + $config = $this->config['aircraft_types'][$type] ?? null; + if (!$config) { + throw new Exception("Unbekannter Flugzeugtyp: {$type}"); + } + + return $this->insert('aircraft', [ + 'user_id' => $userId, + 'aircraft_type' => $type, + 'name' => $name ?: $type, + 'purchase_price' => $price, + 'seat_capacity' => $config['seat_capacity'], + 'range_km' => $config['range_km'], + 'fuel_efficiency' => $config['fuel_efficiency'] + ]); + } + + // Route Methoden + public function getUserRoutes(int $userId): array { + return $this->select( + "SELECT r.*, + a1.iata_code as from_iata, a1.name as from_name, a1.city as from_city, + a2.iata_code as to_iata, a2.name as to_name, a2.city as to_city + FROM routes r + JOIN airports a1 ON r.from_airport_id = a1.id + JOIN airports a2 ON r.to_airport_id = a2.id + WHERE r.user_id = ? AND r.is_active = 1 + ORDER BY r.created_at DESC", + [$userId] + ); + } + + public function createRoute(int $userId, int $fromAirportId, int $toAirportId, int $distance): int { + return $this->insert('routes', [ + 'user_id' => $userId, + 'from_airport_id' => $fromAirportId, + 'to_airport_id' => $toAirportId, + 'distance_km' => $distance, + 'base_price' => $this->calculateBasePrice($distance), + 'demand_level' => 50 + ]); + } + + private function calculateBasePrice(int $distance): float { + return max(50, $distance * 0.10); + } + + // Flug Methoden + public function calculateFlight(int $routeId, int $aircraftId): array { + $route = $this->selectOne( + "SELECT r.*, a1.latitude as from_lat, a1.longitude as from_lon, + a2.latitude as to_lat, a2.longitude as to_lon + FROM routes r + JOIN airports a1 ON r.from_airport_id = a1.id + JOIN airports a2 ON r.to_airport_id = a2.id + WHERE r.id = ?", + [$routeId] + ); + + $aircraft = $this->selectOne("SELECT * FROM aircraft WHERE id = ?", [$aircraftId]); + + if (!$route || !$aircraft) { + throw new Exception("Route oder Flugzeug nicht gefunden"); + } + + $distance = $route['distance_km']; + $fuelCost = $distance * $this->config['game']['fuel_cost_per_km'] * $aircraft['fuel_efficiency']; + $maxPassengers = $aircraft['seat_capacity']; + $basePrice = $route['base_price']; + + $passengers = (int) ($maxPassengers * ($route['demand_level'] / 100) * 0.8); + $revenue = $passengers * $basePrice; + $profit = $revenue - $fuelCost; + + return [ + 'distance' => $distance, + 'passengers' => $passengers, + 'revenue' => $revenue, + 'fuel_cost' => $fuelCost, + 'profit' => $profit, + 'condition_loss' => $distance * 0.001 + ]; + } + + public function executeFlight(int $userId, int $routeId, int $aircraftId): array { + $calc = $this->calculateFlight($routeId, $aircraftId); + $aircraft = $this->selectOne("SELECT * FROM aircraft WHERE id = ?", [$aircraftId]); + + $departure = date('Y-m-d H:i:s'); + $flightTimeHours = $calc['distance'] / 800; + $arrival = date('Y-m-d H:i:s', strtotime("+{$flightTimeHours} hours")); + + $flightId = $this->insert('flights', [ + 'route_id' => $routeId, + 'aircraft_id' => $aircraftId, + 'user_id' => $userId, + 'departure_time' => $departure, + 'arrival_time' => $arrival, + 'passengers' => $calc['passengers'], + 'revenue' => $calc['revenue'], + 'fuel_cost' => $calc['fuel_cost'], + 'profit' => $calc['profit'] + ]); + + // Balance aktualisieren + $this->update('users', ['balance' => $this->config['game']['start_balance']], + 'id = ?', [$userId]); // Placeholder, wird unten korrekt gemacht + + $user = $this->getUserById($userId); + $this->update('users', [ + 'balance' => $user['balance'] + $calc['profit'], + 'experience' => $user['experience'] + $this->config['game']['experience_per_flight'] + + ($calc['passengers'] * $this->config['game']['experience_per_passenger']) + ], 'id = ?', [$userId]); + + // Aircraft Condition aktualisieren + $this->update('aircraft', [ + 'condition' => max(0, $aircraft['condition'] - $calc['condition_loss']), + 'flights_count' => $aircraft['flights_count'] + 1 + ], 'id = ?', [$aircraftId]); + + // Tagesstatistik aktualisieren + $today = date('Y-m-d'); + $existing = $this->selectOne( + "SELECT * FROM daily_stats WHERE user_id = ? AND date = ?", + [$userId, $today] + ); + + if ($existing) { + $this->update('daily_stats', [ + 'total_flights' => $existing['total_flights'] + 1, + 'total_passengers' => $existing['total_passengers'] + $calc['passengers'], + 'total_revenue' => $existing['total_revenue'] + $calc['revenue'], + 'total_profit' => $existing['total_profit'] + $calc['profit'], + 'fuel_cost' => $existing['fuel_cost'] + $calc['fuel_cost'] + ], 'user_id = ? AND date = ?', [$userId, $today]); + } else { + $this->insert('daily_stats', [ + 'user_id' => $userId, + 'date' => $today, + 'total_flights' => 1, + 'total_passengers' => $calc['passengers'], + 'total_revenue' => $calc['revenue'], + 'total_profit' => $calc['profit'], + 'fuel_cost' => $calc['fuel_cost'] + ]); + } + + return array_merge($calc, ['flight_id' => $flightId]); + } + + // Statistik Methoden + public function getUserStats(int $userId): array { + return $this->selectOne( + "SELECT SUM(total_flights) as total_flights, + SUM(total_passengers) as total_passengers, + SUM(total_revenue) as total_revenue, + SUM(total_profit) as total_profit, + SUM(fuel_cost) as total_fuel_cost + FROM daily_stats WHERE user_id = ?", + [$userId] + ) ?? ['total_flights' => 0, 'total_passengers' => 0, 'total_revenue' => 0, 'total_profit' => 0, 'total_fuel_cost' => 0]; + } + + public function getTodayStats(int $userId): array { + return $this->selectOne( + "SELECT * FROM daily_stats WHERE user_id = ? AND date = ?", + [$userId, date('Y-m-d')] + ) ?? ['total_flights' => 0, 'total_passengers' => 0, 'total_revenue' => 0, 'total_profit' => 0]; + } + + // Hilfreiche Methoden + public function calculateDistance(float $lat1, float $lon1, float $lat2, float $lon2): int { + $earthRadius = 6371; // km + $dLat = deg2rad($lat2 - $lat1); + $dLon = deg2rad($lon2 - $lon1); + + $a = sin($dLat / 2) * sin($dLat / 2) + + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * + sin($dLon / 2) * sin($dLon / 2); + + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + + return (int) round($earthRadius * $c); + } + + public function getConfig(): array { + return $this->config; + } +} diff --git a/www/src/Router.php b/www/src/Router.php new file mode 100644 index 0000000..6c2e50e --- /dev/null +++ b/www/src/Router.php @@ -0,0 +1,78 @@ +basePath = $basePath; + } + + public function get(string $path, callable $handler): void { + $this->routes['GET'][$path] = $handler; + } + + public function post(string $path, callable $handler): void { + $this->routes['POST'][$path] = $handler; + } + + public function handle(string $method, string $uri): void { + $method = strtoupper($method); + + // Base Path entfernen + if ($this->basePath && strpos($uri, $this->basePath) === 0) { + $uri = substr($uri, strlen($this->basePath)); + } + + // Query String entfernen + $uri = strtok($uri, '?'); + + // Mehrere Routen durchgehen + foreach ($this->routes[$method] ?? [] as $routePath => $handler) { + $params = $this->matchRoute($routePath, $uri); + if ($params !== false) { + // Parameter als GET-Variablen setzen + $_GET = array_merge($_GET, $params); + $handler(); + return; + } + } + + // 404 Not Found + http_response_code(404); + echo json_encode(['error' => 'Seite nicht gefunden', 'uri' => $uri]); + } + + private function matchRoute(string $routePath, string $uri): array|false { + // Regex-basierte Route Matching + $pattern = preg_replace('/\{([a-zA-Z_]+)\}/', '(?P<$1>[^/]+)', $routePath); + $pattern = '#^' . $pattern . '$#'; + + if (preg_match($pattern, $uri, $matches)) { + // Nur benannte Capture Groups zurückgeben + $params = []; + foreach ($matches as $key => $value) { + if (is_string($key)) { + $params[$key] = $value; + } + } + return $params; + } + + return false; + } + + public function redirect(string $path): void { + header('Location: ' . $this->basePath . $path); + exit; + } + + public function url(string $path = ''): string { + return $this->basePath . $path; + } +} diff --git a/www/src/Session.php b/www/src/Session.php new file mode 100644 index 0000000..b4d358a --- /dev/null +++ b/www/src/Session.php @@ -0,0 +1,121 @@ +getUserById($userId); + if ($user) { + self::set('balance', $user['balance']); + self::set('level', $user['level']); + self::set('experience', $user['experience']); + self::set('airline_name', $user['airline_name']); + } + } + + public static function getCsrfToken(): string { + if (!self::has('csrf_token')) { + self::set('csrf_token', bin2hex(random_bytes(32))); + } + return self::get('csrf_token'); + } + + public static function validateCsrf(string $token): bool { + return self::has('csrf_token') && hash_equals(self::get('csrf_token'), $token); + } + + public static function flash(string $key, mixed $value = null): mixed { + if ($value !== null) { + self::set('flash_' . $key, $value); + return $value; + } + $flashValue = self::get('flash_' . $key); + self::remove('flash_' . $key); + return $flashValue; + } +} diff --git a/www/src/bootstrap.php b/www/src/bootstrap.php new file mode 100644 index 0000000..d4fa1e0 --- /dev/null +++ b/www/src/bootstrap.php @@ -0,0 +1,122 @@ +'; +} + +function validate_csrf(string $token): bool { + return Session::validateCsrf($token); +} + +// Login-Check für geschützte Seiten +function requireLogin(): void { + if (!Session::isLoggedIn()) { + redirect('/login.php?redirect=' . urlencode($_SERVER['REQUEST_URI'])); + } +} + +// Admin-Check (für spätere Phasen) +function requireAdmin(): void { + requireLogin(); + // Hier könnte eine Admin-Prüfung implementiert werden +} diff --git a/www/src/config.php b/www/src/config.php new file mode 100644 index 0000000..ae57e91 --- /dev/null +++ b/www/src/config.php @@ -0,0 +1,89 @@ + [ + 'type' => 'sqlite', + 'path' => dirname(__DIR__) . '/database/game.sqlite', + 'schema' => dirname(__DIR__) . '/database/schema.sql' + ], + + // Anwendung + 'app' => [ + 'name' => 'Airline Tycoon', + 'version' => '1.0.0', + 'timezone' => 'Europe/Berlin', + 'debug' => false + ], + + // Spiel + 'game' => [ + 'start_balance' => 500000.00, + 'start_aircraft' => 'Dash 8-100', + 'fuel_cost_per_km' => 0.15, + 'maintenance_cost_rate' => 0.001, + 'experience_per_flight' => 10, + 'experience_per_passenger' => 1, + 'level_up_threshold' => 1000, + 'level_multiplier' => 1.5 + ], + + // Flugzeugtypen + 'aircraft_types' => [ + 'Dash 8-100' => [ + 'seat_capacity' => 35, + 'range_km' => 1500, + 'purchase_price' => 8500000, + 'fuel_efficiency' => 1.2, + 'speed_kmh' => 500 + ], + 'Boeing 737-300' => [ + 'seat_capacity' => 126, + 'range_km' => 4200, + 'purchase_price' => 45000000, + 'fuel_efficiency' => 1.0, + 'speed_kmh' => 830 + ], + 'Airbus A320' => [ + 'seat_capacity' => 150, + 'range_km' => 5700, + 'purchase_price' => 65000000, + 'fuel_efficiency' => 0.95, + 'speed_kmh' => 840 + ], + 'Boeing 747-400' => [ + 'seat_capacity' => 416, + 'range_km' => 13400, + 'purchase_price' => 220000000, + 'fuel_efficiency' => 0.8, + 'speed_kmh' => 910 + ], + 'Airbus A380' => [ + 'seat_capacity' => 525, + 'range_km' => 15200, + 'purchase_price' => 350000000, + 'fuel_efficiency' => 0.75, + 'speed_kmh' => 900 + ] + ], + + // Sitzplatzklassen + 'seat_classes' => [ + 'economy' => ['multiplier' => 1.0, 'comfort' => 1.0], + 'business' => ['multiplier' => 2.5, 'comfort' => 1.5], + 'first' => ['multiplier' => 5.0, 'comfort' => 2.0] + ], + + // Pfade + 'paths' => [ + 'root' => dirname(__DIR__), + 'www' => dirname(__DIR__) . '/www/public', + 'assets' => dirname(__DIR__) . '/www/public/assets', + 'data' => dirname(__DIR__) . '/www/data' + ] +];