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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Klicke oder tippe auf einen Flughafen um eine Route zu erstellen.
+
+
+
+
+
+
+
+
+
+
+
Flughafen-Info
+
Wähle einen Flughafen auf der Karte
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | IATA |
+ Name |
+ Stadt |
+ Land |
+ Kontinent |
+ Passagiere/Jahr |
+
+
+
+
+
+ | = h($airport['iata_code']) ?> |
+ = h($airport['name']) ?> |
+ = h($airport['city']) ?> |
+ = h($airport['country']) ?> |
+ = h($airport['continent']) ?> |
+ = formatNumber($airport['passenger_volume']) ?> |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Du hast noch keine Flugzeuge.
+
+ 🛒 Flugzeug kaufen
+
+
+
+
+
+
+
+
✈️
+
+
= h($plane['name']) ?>
+
= h($plane['aircraft_type']) ?>
+
+
+
+
+
+ Sitze: = $plane['seat_capacity'] ?>
+
+
+ Reichweite: = formatNumber($plane['range_km']) ?> km
+
+
+ Zustand:
+
+ = number_format($plane['condition'], 1, ',', '.') ?>%
+
+
+
+ Flüge: = formatNumber($plane['flights_count']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Du hast noch keine Routen.
+
+ 🗺️ Route erstellen
+
+
+
+
+
+
+
+
+ = h($route['from_iata']) ?>
+ = h($route['from_city']) ?>
+
+
✈️
+
+ = h($route['to_iata']) ?>
+ = h($route['to_city']) ?>
+
+
+
+
+ Distanz:
+ = formatNumber($route['distance_km']) ?> km
+
+
+
Nachfrage:
+
+
+ = $route['demand_level'] ?>%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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, = h($user['username']) ?>!
+
+ Level = h($user['level']) ?> | = h($user['airline_name']) ?>
+
+
+
+
+
+ Erfahrung
+ = number_format($user['experience'], 0, ',', '.') ?> / = number_format($expForNextLevel, 0, ',', '.') ?> XP
+
+
+
+ = number_format($levelProgress, 1, ',', '.') ?>%
+
+
+
+
+
+
+
+
+
💰
+
= formatMoney($user['balance']) ?>
+
Kontostand
+
+
+
+
✈️
+
= count($aircraft) ?>
+
Flugzeuge
+
+
+
+
🛫
+
= count($routes) ?>
+
Routen
+
+
+
+
👥
+
= formatNumber($todayStats['total_passengers'] ?? 0) ?>
+
Passagiere (heute)
+
+
+
+
📈
+
= formatMoney($todayStats['total_profit'] ?? 0) ?>
+
Gewinn (heute)
+
+
+
+
🏆
+
= formatNumber($stats['total_flights'] ?? 0) ?>
+
Flüge (gesamt)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Du hast noch keine Flugzeuge.
+
+
+
+
+
+
+
+ | Name |
+ Typ |
+ Zustand |
+ Flüge |
+ Aktion |
+
+
+
+
+
+ | = h($plane['name']) ?> |
+ = h($plane['aircraft_type']) ?> |
+
+
+ = number_format($plane['condition'], 1, ',', '.') ?>%
+
+ |
+ = formatNumber($plane['flights_count']) ?> |
+
+
+
+
+ Keine Route
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Du hast noch keine Routen.
+
+ 🛫 Jetzt Route erstellen
+
+
+
+
+
+
+
+ | Von |
+ Nach |
+ Distanz |
+ Nachfrage |
+
+
+
+
+
+
+ = h($route['from_iata']) ?>
+ = h($route['from_city']) ?>
+ |
+
+ = h($route['to_iata']) ?>
+ = h($route['to_city']) ?>
+ |
+ = formatNumber($route['distance_km']) ?> km |
+
+
+
+ = $route['demand_level'] ?>%
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+ | Flug |
+ Flugzeug |
+ Passagiere |
+ Umsatz |
+ Gewinn |
+ Zeit |
+
+
+
+
+
+ |
+ = h($flight['from_iata']) ?> →
+ = h($flight['to_iata']) ?>
+ |
+ = h($flight['aircraft_name']) ?> |
+ = formatNumber($flight['passengers']) ?> |
+ = formatMoney($flight['revenue']) ?> |
+
+ = formatMoney($flight['profit']) ?>
+ |
+ = timeAgo($flight['created_at']) ?> |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dein Kontostand: = formatMoney($user['balance']) ?>
+
+
+ $specs): ?>
+ = $specs['purchase_price'];
+ ?>
+
+
+
+
= h($type) ?>
+
+ Sitze: = $specs['seat_capacity'] ?> |
+ Reichweite: = formatNumber($specs['range_km']) ?> km
+
+
+
+
= formatMoney($specs['purchase_price']) ?>
+
+
+
+
Zu teuer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Wähle ein Flugzeug für diese Route:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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!
+
+
+
+
+
+
+
+
+
+
+
+
+
🛫
+
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!
+
+
+
+
+
+
+
+
+
+
+
+
+
📊 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!
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
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'
+ ]
+];