feat(phase1): complete base structure - landing page, auth, dashboard, airports, API, DB schema, 60 airports CSV

This commit is contained in:
2026-04-24 15:53:59 +02:00
parent 2960bd4c2f
commit ab5f97acbf
18 changed files with 4926 additions and 0 deletions

116
database/schema.sql Normal file
View File

@@ -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);

61
www/data/airports.csv Normal file
View File

@@ -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
1 id iata_code name city country continent latitude longitude timezone passenger_volume demand_multiplier
2 1 FRA Frankfurt Airport Frankfurt Germany Europe 50.0379 8.5622 Europe/Berlin 70000000 1.50
3 2 MUC Munich Airport Munich Germany Europe 48.3537 11.7861 Europe/Berlin 50000000 1.30
4 3 CDG Charles de Gaulle Airport Paris France Europe 49.0097 2.5479 Europe/Paris 76000000 1.40
5 4 AMS Amsterdam Airport Schiphol Amsterdam Netherlands Europe 52.3105 4.7683 Europe/Amsterdam 72000000 1.35
6 5 LHR London Heathrow Airport London UK Europe 51.4700 -0.4543 Europe/London 80000000 1.45
7 6 MAD Adolfo Suárez Madrid-Barajas Airport Madrid Spain Europe 40.4983 -3.5676 Europe/Madrid 60000000 1.25
8 7 BCN Barcelona-El Prat Airport Barcelona Spain Europe 41.2974 2.0833 Europe/Madrid 50000000 1.30
9 8 FCO Leonardo da Vinci-Fiumicino Airport Rome Italy Europe 41.8003 12.2389 Europe/Rome 49000000 1.25
10 9 ZRH Zurich Airport Zurich Switzerland Europe 47.4647 8.5492 Europe/Zurich 31000000 1.20
11 10 VIE Vienna International Airport Vienna Austria Europe 48.1103 16.5697 Europe/Vienna 31000000 1.15
12 11 CPH Copenhagen Airport Copenhagen Denmark Europe 55.6180 12.6560 Europe/Copenhagen 30000000 1.15
13 12 OSL Oslo Airport, Gardermoen Oslo Norway Europe 60.1939 11.1004 Europe/Oslo 29000000 1.10
14 13 ARN Stockholm Arlanda Airport Stockholm Sweden Europe 59.6519 17.9186 Europe/Stockholm 28000000 1.10
15 14 HEL Helsinki-Vantaa Airport Helsinki Finland Europe 60.3172 24.9633 Europe/Helsinki 22000000 1.05
16 15 WAW Warsaw Chopin Airport Warsaw Poland Europe 52.1657 20.9671 Europe/Warsaw 19000000 1.10
17 16 JFK John F. Kennedy International Airport New York USA North America 40.6413 -73.7781 America/New_York 62000000 1.60
18 17 LAX Los Angeles International Airport Los Angeles USA North America 33.9425 -118.4081 America/Los_Angeles 87000000 1.55
19 18 ORD O'Hare International Airport Chicago USA North America 41.9742 -87.9073 America/Chicago 84000000 1.50
20 19 ATL Hartsfield-Jackson Atlanta International Airport Atlanta USA North America 33.6407 -84.4277 America/New_York 107000000 1.45
21 20 DFW Dallas/Fort Worth International Airport Dallas USA North America 32.8998 -97.0403 America/Chicago 75000000 1.40
22 21 DEN Denver International Airport Denver USA North America 39.8561 -104.6737 America/Denver 69000000 1.35
23 22 SFO San Francisco International Airport San Francisco USA North America 37.6213 -122.3790 America/Los_Angeles 58000000 1.45
24 23 SEA Seattle-Tacoma International Airport Seattle USA North America 47.4502 -122.3088 America/Los_Angeles 51000000 1.30
25 24 MIA Miami International Airport Miami USA North America 25.7959 -80.2870 America/New_York 45000000 1.40
26 25 BOS Boston Logan International Airport Boston USA North America 42.3656 -71.0096 America/New_York 42000000 1.30
27 26 MEX Mexico City International Airport Mexico City Mexico North America 19.4363 -99.0721 America/Mexico_City 50000000 1.35
28 27 YYZ Toronto Pearson International Airport Toronto Canada North America 43.6777 -79.6248 America/Toronto 50000000 1.30
29 28 YVR Vancouver International Airport Vancouver Canada North America 49.1967 -123.1815 America/Vancouver 26000000 1.20
30 29 GRU São Paulo/Guarulhos International Airport São Paulo Brazil South America -23.4356 -46.4731 America/Sao_Paulo 43000000 1.40
31 30 EZE Ministro Pistarini International Airport Buenos Aires Argentina South America -34.8222 -58.5358 America/Argentina/Buenos_Aires 13000000 1.20
32 31 BOG El Dorado International Airport Bogotá Colombia South America 4.7016 -74.1469 America/Bogota 35000000 1.25
33 32 LIM Jorge Chávez International Airport Lima Peru South America -12.0219 -77.1143 America/Lima 22000000 1.15
34 33 UIO Mariscal Sucre International Airport Quito Ecuador South America -0.1412 -78.4892 America/Guayaquil 7000000 1.00
35 34 SYD Sydney Kingsford Smith Airport Sydney Australia Australia -33.9399 151.1753 Australia/Sydney 44000000 1.35
36 35 MEL Melbourne Airport Melbourne Australia Australia -37.6690 144.8410 Australia/Melbourne 37000000 1.30
37 36 SIN Singapore Changi Airport Singapore Singapore Asia 1.3644 103.9915 Asia/Singapore 65000000 1.55
38 37 HKG Hong Kong International Airport Hong Kong Hong Kong Asia 22.3080 113.9185 Asia/Hong_Kong 75000000 1.50
39 38 NRT Narita International Airport Tokyo Japan Asia 35.7720 140.3929 Asia/Tokyo 43000000 1.45
40 39 ICN Incheon International Airport Seoul South Korea Asia 37.4602 126.4407 Asia/Seoul 72000000 1.50
41 40 PEK Beijing Capital International Airport Beijing China Asia 40.0799 116.6031 Asia/Shanghai 100000000 1.60
42 41 PVG Shanghai Pudong International Airport Shanghai China Asia 31.1443 121.8083 Asia/Shanghai 74000000 1.50
43 42 CAN Guangzhou Baiyun International Airport Guangzhou China Asia 23.3924 113.2988 Asia/Shanghai 73000000 1.45
44 43 SZX Shenzhen Bao'an International Airport Shenzhen China Asia 22.6393 113.8108 Asia/Shanghai 50000000 1.40
45 44 BKK Suvarnabhumi Airport Bangkok Thailand Asia 13.6900 100.7501 Asia/Bangkok 60000000 1.40
46 45 KUL Kuala Lumpur International Airport Kuala Lumpur Malaysia Asia 2.7456 101.7072 Asia/Kuala_Lumpur 59000000 1.35
47 46 DEL Indira Gandhi International Airport New Delhi India Asia 28.5562 77.1000 Asia/Kolkata 69000000 1.45
48 47 BOM Chhatrapati Shivaji Maharaj International Airport Mumbai India Asia 19.0896 72.8656 Asia/Kolkata 49000000 1.40
49 48 DXB Dubai International Airport Dubai UAE Asia 25.2532 55.3657 Asia/Dubai 89000000 1.60
50 49 AUH Abu Dhabi International Airport Abu Dhabi UAE Asia 24.4330 54.6511 Asia/Dubai 30000000 1.30
51 50 DOH Hamad International Airport Doha Qatar Asia 25.2609 51.6138 Asia/Qatar 53000000 1.40
52 51 IST Istanbul Airport Istanbul Turkey Europe 41.2753 28.7519 Europe/Istanbul 76000000 1.45
53 52 ATH Athens International Airport Athens Greece Europe 37.9364 23.9445 Europe/Athens 25000000 1.15
54 53 TLV Ben Gurion International Airport Tel Aviv Israel Asia 32.0055 34.8854 Asia/Jerusalem 25000000 1.20
55 54 CAI Cairo International Airport Cairo Egypt Africa 30.1219 31.4056 Africa/Cairo 30000000 1.25
56 55 JNB O. R. Tambo International Airport Johannesburg South Africa Africa -26.1392 28.2460 Africa/Johannesburg 22000000 1.20
57 56 CPT Cape Town International Airport Cape Town South Africa Africa -33.9715 18.6021 Africa/Johannesburg 10000000 1.10
58 57 NBO Jomo Kenyatta International Airport Nairobi Kenya Africa -1.3192 36.9275 Africa/Nairobi 8000000 1.05
59 58 CMN Mohammed V International Airport Casablanca Morocco Africa 33.3675 -7.5900 Africa/Casablanca 9000000 1.10
60 59 LOS Murtala Muhammed International Airport Lagos Nigeria Africa 6.5774 3.3212 Africa/Lagos 7000000 1.05
61 60 ADD Addis Ababa Bole International Airport Addis Ababa Ethiopia Africa 8.9779 38.7993 Africa/Addis_Ababa 12000000 1.15

373
www/public/airports.php Normal file
View File

@@ -0,0 +1,373 @@
<?php
/**
* Airline Tycoon - Airports & World Map
* Interaktive Weltkarte mit Touch-Support
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
requireLogin();
$userId = Session::getUserId();
$user = $db->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.';
}
}
}
?>
<!DOCTYPE html>
<html lang="de" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Airline Tycoon Weltkarte - Alle Flughäfen">
<meta name="csrf-token" content="<?= h(csrf_token()) ?>">
<title>Weltkarte - Airline Tycoon</title>
<link rel="stylesheet" href="<?= BASE_PATH ?>assets/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✈️</text></svg>">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="navbar-container">
<a href="<?= BASE_PATH ?>dashboard.php" class="navbar-brand">✈️ <?= h($user['airline_name']) ?></a>
<button class="navbar-toggle" aria-label="Menü">☰</button>
<ul class="navbar-menu">
<li><a href="<?= BASE_PATH ?>dashboard.php" class="navbar-item">📊 Dashboard</a></li>
<li><a href="<?= BASE_PATH ?>airports.php" class="navbar-item active">🌍 Weltkarte</a></li>
<li><a href="<?= BASE_PATH ?>airports.php?action=fleet" class="navbar-item">✈️ Flotte</a></li>
<li><a href="<?= BASE_PATH ?>airports.php?action=routes" class="navbar-item">🛫 Routen</a></li>
<li>
<span class="navbar-item">
💰 <?= formatMoney($user['balance']) ?>
</span>
</li>
<li>
<button class="theme-toggle" id="themeToggle" aria-label="Design wechseln">🌙</button>
</li>
</ul>
</div>
</nav>
<main class="container p-lg">
<?php if ($message): ?>
<div class="panel mb-lg" style="background: rgba(78, 205, 196, 0.2); border-color: var(--comic-blue);">
<p> <?= h($message) ?></p>
</div>
<?php endif; ?>
<!-- Tabs -->
<div class="d-flex gap-sm mb-lg">
<a href="<?= BASE_PATH ?>airports.php" class="btn <?= !isset($_GET['action']) ? 'btn-primary' : 'btn-outline' ?>">
🗺️ Weltkarte
</a>
<a href="<?= BASE_PATH ?>airports.php?action=fleet" class="btn <?= ($_GET['action'] ?? '') === 'fleet' ? 'btn-primary' : 'btn-outline' ?>">
✈️ Flotte
</a>
<a href="<?= BASE_PATH ?>airports.php?action=routes" class="btn <?= ($_GET['action'] ?? '') === 'routes' ? 'btn-primary' : 'btn-outline' ?>">
🛫 Routen
</a>
</div>
<?php if (!isset($_GET['action']) || $_GET['action'] === 'map'): ?>
<!-- Weltkarte -->
<div class="panel">
<h2 class="panel-header">🗺️ Interaktive Weltkarte</h2>
<p class="text-secondary mb-md">
Klicke oder tippe auf einen Flughafen um eine Route zu erstellen.
</p>
<div class="d-flex gap-lg">
<!-- Karte -->
<div id="mapContainer" style="flex: 2; min-height: 500px; position: relative;">
<!-- Map wird per JS initialisiert -->
</div>
<!-- Flughafen Info -->
<div id="airportInfo" class="card" style="flex: 1; min-width: 250px; display: none;">
<h3>Flughafen-Info</h3>
<p class="text-muted">Wähle einen Flughafen auf der Karte</p>
</div>
</div>
</div>
<!-- Routen erstellen Panel -->
<div class="panel mt-lg">
<h2 class="panel-header">🛫 Neue Route erstellen</h2>
<form method="POST" class="d-flex flex-wrap gap-md mt-md">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="action" value="create_route">
<div class="form-group" style="flex: 1; min-width: 200px;">
<label class="form-label" for="fromAirport">Von</label>
<select id="fromAirport" name="from_airport" class="form-select" required>
<option value="">Flughafen wählen...</option>
<?php foreach ($airports as $airport): ?>
<option value="<?= $airport['id'] ?>">
<?= h($airport['iata_code']) ?> - <?= h($airport['city']) ?>, <?= h($airport['country']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="d-flex flex-center" style="padding-top: 24px;">
<span style="font-size: 2rem;">→</span>
</div>
<div class="form-group" style="flex: 1; min-width: 200px;">
<label class="form-label" for="toAirport">Nach</label>
<select id="toAirport" name="to_airport" class="form-select" required>
<option value="">Flughafen wählen...</option>
<?php foreach ($airports as $airport): ?>
<option value="<?= $airport['id'] ?>">
<?= h($airport['iata_code']) ?> - <?= h($airport['city']) ?>, <?= h($airport['country']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="d-flex flex-center" style="padding-top: 24px;">
<button type="submit" class="btn btn-primary">
✈️ Route erstellen
</button>
</div>
</form>
</div>
<!-- Alle Flughäfen Liste -->
<div class="panel mt-lg">
<h2 class="panel-header">📋 Alle Flughäfen (<?= count($airports) ?>)</h2>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>IATA</th>
<th>Name</th>
<th>Stadt</th>
<th>Land</th>
<th>Kontinent</th>
<th>Passagiere/Jahr</th>
</tr>
</thead>
<tbody>
<?php foreach ($airports as $airport): ?>
<tr onclick="selectAirportFromList(<?= $airport['id'] ?>)" style="cursor: pointer;">
<td><strong><?= h($airport['iata_code']) ?></strong></td>
<td><?= h($airport['name']) ?></td>
<td><?= h($airport['city']) ?></td>
<td><?= h($airport['country']) ?></td>
<td><?= h($airport['continent']) ?></td>
<td><?= formatNumber($airport['passenger_volume']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php elseif ($_GET['action'] === 'fleet'): ?>
<!-- Flottenverwaltung -->
<div class="panel">
<h2 class="panel-header">✈️ Meine Flotte</h2>
<?php if (empty($aircraft)): ?>
<div class="card text-center p-lg">
<p class="text-muted">Du hast noch keine Flugzeuge.</p>
<a href="<?= BASE_PATH ?>dashboard.php" class="btn btn-primary mt-md">
🛒 Flugzeug kaufen
</a>
</div>
<?php else: ?>
<div class="d-grid" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--space-md);">
<?php foreach ($aircraft as $plane): ?>
<div class="card">
<div class="d-flex gap-md">
<div class="card-icon">✈️</div>
<div>
<h3><?= h($plane['name']) ?></h3>
<p class="text-secondary"><?= h($plane['aircraft_type']) ?></p>
</div>
</div>
<div class="d-grid mt-md" style="grid-template-columns: 1fr 1fr; gap: var(--space-sm);">
<div>
<strong>Sitze:</strong> <?= $plane['seat_capacity'] ?>
</div>
<div>
<strong>Reichweite:</strong> <?= formatNumber($plane['range_km']) ?> km
</div>
<div>
<strong>Zustand:</strong>
<span class="badge <?= $plane['condition'] > 70 ? 'badge-success' : ($plane['condition'] > 30 ? 'badge-warning' : 'badge-danger') ?>">
<?= number_format($plane['condition'], 1, ',', '.') ?>%
</span>
</div>
<div>
<strong>Flüge:</strong> <?= formatNumber($plane['flights_count']) ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($_GET['action'] === 'routes'): ?>
<!-- Routenverwaltung -->
<div class="panel">
<h2 class="panel-header">🛫 Meine Routen</h2>
<?php if (empty($routes)): ?>
<div class="card text-center p-lg">
<p class="text-muted">Du hast noch keine Routen.</p>
<a href="<?= BASE_PATH ?>airports.php" class="btn btn-primary mt-md">
🗺️ Route erstellen
</a>
</div>
<?php else: ?>
<div class="d-grid" style="grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--space-md);">
<?php foreach ($routes as $route): ?>
<div class="card">
<div class="d-flex flex-center gap-md">
<div class="text-center">
<strong><?= h($route['from_iata']) ?></strong><br>
<small><?= h($route['from_city']) ?></small>
</div>
<div style="font-size: 1.5rem;">✈️</div>
<div class="text-center">
<strong><?= h($route['to_iata']) ?></strong><br>
<small><?= h($route['to_city']) ?></small>
</div>
</div>
<div class="d-flex flex-between mt-md">
<span>Distanz:</span>
<strong><?= formatNumber($route['distance_km']) ?> km</strong>
</div>
<div class="d-flex flex-between">
<span>Nachfrage:</span>
<div class="progress" style="width: 100px; height: 16px;">
<div class="progress-bar" style="width: <?= $route['demand_level'] ?>%">
<?= $route['demand_level'] ?>%
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</main>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
<script>
const AIRPORTS_DATA = <?= json_encode($airports) ?>;
const ROUTES_DATA = <?= json_encode($routes) ?>;
const AIRCRAFT_DATA = <?= json_encode($aircraft) ?>;
</script>
<script src="<?= BASE_PATH ?>assets/js/main.js"></script>
<script>
// Airport aus Liste auswählen
function selectAirportFromList(airportId) {
const airport = AIRPORTS_DATA.find(a => a.id == airportId);
if (airport && typeof selectAirport === 'function') {
selectAirport(airport);
// Scroll zur Karte
document.querySelector('#mapContainer')?.scrollIntoView({ behavior: 'smooth' });
}
}
// Event Listener für Airport-Auswahl
document.addEventListener('airportSelected', function(e) {
const airport = e.detail;
const infoPanel = document.getElementById('airportInfo');
if (infoPanel && airport) {
infoPanel.style.display = 'block';
infoPanel.innerHTML = `
<h3>✈️ ${airport.iata_code}</h3>
<p><strong>${airport.name}</strong></p>
<p>${airport.city}, ${airport.country}</p>
<hr>
<p><strong>Kontinent:</strong> ${airport.continent}</p>
<p><strong>Passagiere:</strong> ${formatNumber(airport.passenger_volume)}/Jahr</p>
<p><strong>Nachfrage:</strong> ${(airport.demand_multiplier * 100).toFixed(0)}%</p>
<div class="mt-md">
<p><strong>Von hier:</strong></p>
<select class="form-select" onchange="setFromAirport(${airport.id}, this.value)">
<option value="">Route von ${airport.iata_code} nach...</option>
${AIRPORTS_DATA.filter(a => a.id != airport.id).map(a =>
`<option value="${a.id}">${a.iata_code} - ${a.city}</option>`
).join('')}
</select>
</div>
`;
}
});
function setFromAirport(fromId, toId) {
if (toId) {
document.querySelector('select[name="from_airport"]').value = fromId;
document.querySelector('select[name="to_airport"]').value = toId;
}
}
</script>
</body>
</html>

142
www/public/api/airports.php Normal file
View File

@@ -0,0 +1,142 @@
<?php
/**
* Airline Tycoon - Airports API
* Flughafen-bezogene API-Endpunkte
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? $_POST['action'] ?? '';
switch ($action) {
case 'list':
handleList();
break;
case 'get':
handleGet();
break;
case 'search':
handleSearch();
break;
case 'distance':
handleDistance();
break;
default:
jsonResponse(['error' => '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
]);
}

189
www/public/api/auth.php Normal file
View File

@@ -0,0 +1,189 @@
<?php
/**
* Airline Tycoon - Auth API
* Authentifizierungs-bezogene API-Endpunkte
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
// Nur JSON-Antworten
if (!isAjax()) {
jsonResponse(['error' => '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()
]);
}

331
www/public/api/game.php Normal file
View File

@@ -0,0 +1,331 @@
<?php
/**
* Airline Tycoon - Game API
* Hauptspiel-API für Flüge, Routen, Flugzeuge, etc.
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
// Auth Check für alle Aktionen außer 'status'
$action = $_GET['action'] ?? $_POST['action'] ?? '';
$publicActions = ['status', 'info'];
if (!in_array($action, $publicActions) && !Session::isLoggedIn()) {
jsonResponse(['error' => '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)
]);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 = `
<span>${getToastIcon(type)}</span>
<span>${escapeHtml(message)}</span>
`;
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 = '<span class="loading"></span>';
}
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 = `
<button class="map-btn" id="mapZoomIn" title="Zoomen">+</button>
<button class="map-btn" id="mapZoomOut" title="Verkleinern"></button>
<button class="map-btn" id="mapReset" title="Zurücksetzen">⟲</button>
`;
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 = `
<strong>${airport.iata_code}</strong> - ${airport.name}<br>
<small>${airport.city}, ${airport.country}</small><br>
<small>Passagiere: ${formatNumber(airport.passenger_volume)}/Jahr</small>
`;
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 = `
<h3>${airport.iata_code} - ${airport.name}</h3>
<p><strong>Stadt:</strong> ${airport.city}, ${airport.country}</p>
<p><strong>Kontinent:</strong> ${airport.continent}</p>
<p><strong>Passagiere:</strong> ${formatNumber(airport.passenger_volume)}/Jahr</p>
<p><strong>Nachfrage:</strong> ${(airport.demand_multiplier * 100).toFixed(0)}%</p>
<button class="btn btn-primary mt-md" onclick="createRouteFromAirport(${airport.id})">
Route erstellen
</button>
`;
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;
})();

461
www/public/dashboard.php Normal file
View File

@@ -0,0 +1,461 @@
<?php
/**
* Airline Tycoon - Dashboard
* Hauptseite für eingeloggte Spieler
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
requireLogin();
$userId = Session::getUserId();
$user = $db->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;
?>
<!DOCTYPE html>
<html lang="de" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Airline Tycoon Dashboard - Verwalte deine Airline">
<meta name="csrf-token" content="<?= h(csrf_token()) ?>">
<title>Dashboard - Airline Tycoon</title>
<link rel="stylesheet" href="<?= BASE_PATH ?>assets/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✈️</text></svg>">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="navbar-container">
<a href="<?= BASE_PATH ?>dashboard.php" class="navbar-brand">✈️ <?= h($user['airline_name']) ?></a>
<button class="navbar-toggle" aria-label="Menü">☰</button>
<ul class="navbar-menu">
<li><a href="<?= BASE_PATH ?>dashboard.php" class="navbar-item active">📊 Dashboard</a></li>
<li><a href="<?= BASE_PATH ?>airports.php" class="navbar-item">🌍 Weltkarte</a></li>
<li><a href="<?= BASE_PATH ?>airports.php?action=fleet" class="navbar-item">✈️ Flotte</a></li>
<li><a href="<?= BASE_PATH ?>airports.php?action=routes" class="navbar-item">🛫 Routen</a></li>
<li>
<span class="navbar-item">
💰 <?= formatMoney($user['balance']) ?>
</span>
</li>
<li>
<a href="<?= BASE_PATH ?>logout.php" class="navbar-item">🚪 Abmelden</a>
</li>
<li>
<button class="theme-toggle" id="themeToggle" aria-label="Design wechseln">🌙</button>
</li>
</ul>
</div>
</nav>
<main class="container p-lg">
<!-- Willkommensnachricht -->
<div class="panel mb-lg">
<h1>👋 Willkommen, <?= h($user['username']) ?>!</h1>
<p class="mt-sm text-secondary">
Level <?= h($user['level']) ?> | <?= h($user['airline_name']) ?>
</p>
<!-- Level Fortschritt -->
<div class="mt-md">
<div class="d-flex flex-between mb-sm">
<span>Erfahrung</span>
<span><?= number_format($user['experience'], 0, ',', '.') ?> / <?= number_format($expForNextLevel, 0, ',', '.') ?> XP</span>
</div>
<div class="progress">
<div class="progress-bar" style="width: <?= min(100, $levelProgress) ?>%">
<?= number_format($levelProgress, 1, ',', '.') ?>%
</div>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid mb-lg">
<div class="stat-card">
<div class="stat-icon">💰</div>
<div class="stat-value"><?= formatMoney($user['balance']) ?></div>
<div class="stat-label">Kontostand</div>
</div>
<div class="stat-card">
<div class="stat-icon">✈️</div>
<div class="stat-value"><?= count($aircraft) ?></div>
<div class="stat-label">Flugzeuge</div>
</div>
<div class="stat-card">
<div class="stat-icon">🛫</div>
<div class="stat-value"><?= count($routes) ?></div>
<div class="stat-label">Routen</div>
</div>
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-value"><?= formatNumber($todayStats['total_passengers'] ?? 0) ?></div>
<div class="stat-label">Passagiere (heute)</div>
</div>
<div class="stat-card">
<div class="stat-icon">📈</div>
<div class="stat-value"><?= formatMoney($todayStats['total_profit'] ?? 0) ?></div>
<div class="stat-label">Gewinn (heute)</div>
</div>
<div class="stat-card">
<div class="stat-icon">🏆</div>
<div class="stat-value"><?= formatNumber($stats['total_flights'] ?? 0) ?></div>
<div class="stat-label">Flüge (gesamt)</div>
</div>
</div>
<!-- Quick Actions -->
<div class="panel mb-lg">
<h2 class="panel-header">⚡ Schnellaktionen</h2>
<div class="d-flex flex-wrap gap-md mt-md">
<a href="<?= BASE_PATH ?>airports.php" class="btn btn-primary">
🛫 Neue Route erstellen
</a>
<button class="btn btn-secondary" onclick="openModal('purchaseAircraftModal')">
🛒 Flugzeug kaufen
</button>
<a href="<?= BASE_PATH ?>airports.php" class="btn btn-outline">
🌍 Weltkarte ansehen
</a>
</div>
</div>
<!-- Aircraft & Routes -->
<div class="d-grid" style="grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: var(--space-lg);">
<!-- Meine Flugzeuge -->
<div class="panel">
<h2 class="panel-header">✈️ Meine Flotte</h2>
<?php if (empty($aircraft)): ?>
<div class="card text-center p-lg">
<p class="text-muted">Du hast noch keine Flugzeuge.</p>
<button class="btn btn-primary mt-md" onclick="openModal('purchaseAircraftModal')">
🛒 Jetzt Flugzeug kaufen
</button>
</div>
<?php else: ?>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Typ</th>
<th>Zustand</th>
<th>Flüge</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($aircraft as $plane): ?>
<tr>
<td><?= h($plane['name']) ?></td>
<td><?= h($plane['aircraft_type']) ?></td>
<td>
<span class="badge <?= $plane['condition'] > 70 ? 'badge-success' : ($plane['condition'] > 30 ? 'badge-warning' : 'badge-danger') ?>">
<?= number_format($plane['condition'], 1, ',', '.') ?>%
</span>
</td>
<td><?= formatNumber($plane['flights_count']) ?></td>
<td>
<?php
// Finde eine Route für dieses Flugzeug
$hasRoute = false;
foreach ($routes as $route) {
$hasRoute = true;
break;
}
if ($hasRoute): ?>
<button class="btn btn-sm btn-success"
onclick="startFlightFromAircraft(<?= $plane['id'] ?>)">
▶️ Fliegen
</button>
<?php else: ?>
<span class="text-muted">Keine Route</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Meine Routen -->
<div class="panel">
<h2 class="panel-header">🛫 Meine Routen</h2>
<?php if (empty($routes)): ?>
<div class="card text-center p-lg">
<p class="text-muted">Du hast noch keine Routen.</p>
<a href="<?= BASE_PATH ?>airports.php" class="btn btn-primary mt-md">
🛫 Jetzt Route erstellen
</a>
</div>
<?php else: ?>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Von</th>
<th>Nach</th>
<th>Distanz</th>
<th>Nachfrage</th>
</tr>
</thead>
<tbody>
<?php foreach ($routes as $route): ?>
<tr>
<td>
<strong><?= h($route['from_iata']) ?></strong>
<br><small><?= h($route['from_city']) ?></small>
</td>
<td>
<strong><?= h($route['to_iata']) ?></strong>
<br><small><?= h($route['to_city']) ?></small>
</td>
<td><?= formatNumber($route['distance_km']) ?> km</td>
<td>
<div class="progress" style="width: 80px; height: 12px;">
<div class="progress-bar" style="width: <?= $route['demand_level'] ?>%">
<?= $route['demand_level'] ?>%
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Letzte Flüge -->
<div class="panel mt-lg">
<h2 class="panel-header">📜 Letzte Flüge</h2>
<?php
$recentFlights = $db->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]
);
?>
<?php if (empty($recentFlights)): ?>
<p class="text-center text-muted p-lg">Noch keine Flüge durchgeführt.</p>
<?php else: ?>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Flug</th>
<th>Flugzeug</th>
<th>Passagiere</th>
<th>Umsatz</th>
<th>Gewinn</th>
<th>Zeit</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentFlights as $flight): ?>
<tr>
<td>
<strong><?= h($flight['from_iata']) ?></strong> →
<strong><?= h($flight['to_iata']) ?></strong>
</td>
<td><?= h($flight['aircraft_name']) ?></td>
<td><?= formatNumber($flight['passengers']) ?></td>
<td><?= formatMoney($flight['revenue']) ?></td>
<td class="<?= $flight['profit'] >= 0 ? 'text-success' : 'text-danger' ?>">
<?= formatMoney($flight['profit']) ?>
</td>
<td><?= timeAgo($flight['created_at']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</main>
<!-- Flugzeug kaufen Modal -->
<div id="purchaseAircraftModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">🛒 Flugzeug kaufen</h3>
<button class="modal-close" onclick="closeModal('purchaseAircraftModal')">×</button>
</div>
<div class="modal-body">
<p class="mb-md">Dein Kontostand: <strong><?= formatMoney($user['balance']) ?></strong></p>
<div class="d-grid gap-md">
<?php foreach ($config['aircraft_types'] as $type => $specs): ?>
<?php
$canAfford = $user['balance'] >= $specs['purchase_price'];
?>
<div class="card <?= $canAfford ? '' : 'text-muted' ?>"
style="<?= $canAfford ? '' : 'opacity: 0.6;' ?>">
<div class="d-flex flex-between flex-center">
<div>
<h4><?= h($type) ?></h4>
<p class="text-sm">
Sitze: <?= $specs['seat_capacity'] ?> |
Reichweite: <?= formatNumber($specs['range_km']) ?> km
</p>
</div>
<div class="text-right">
<div class="stat-value text-primary"><?= formatMoney($specs['purchase_price']) ?></div>
<?php if ($canAfford): ?>
<button class="btn btn-sm btn-success mt-sm"
onclick="purchaseAircraft('<?= h($type) ?>')">
Kaufen
</button>
<?php else: ?>
<span class="badge badge-danger">Zu teuer</span>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<!-- Flug starten Modal -->
<div id="flightModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">▶️ Flug starten</h3>
<button class="modal-close" onclick="closeModal('flightModal')">×</button>
</div>
<div class="modal-body">
<p>Wähle ein Flugzeug für diese Route:</p>
<div id="flightAircraftList" class="d-grid gap-md mt-md">
<!-- Wird per JavaScript gefüllt -->
</div>
</div>
</div>
</div>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
<script>
// Globale Daten für JavaScript
const GAME_DATA = {
routes: <?= json_encode($routes) ?>,
aircraft: <?= json_encode($aircraft) ?>,
userBalance: <?= $user['balance'] ?>
};
function startFlightFromAircraft(aircraftId) {
const modal = document.getElementById('flightModal');
const list = document.getElementById('flightAircraftList');
// Gefilterte Liste erstellen
const aircraftHtml = GAME_DATA.aircraft.map(ac => {
if (ac.id == aircraftId) {
return `
<div class="card">
<div class="d-flex flex-between flex-center">
<div>
<h4>${ac.name}</h4>
<p class="text-sm">${ac.aircraft_type} | Sitze: ${ac.seat_capacity}</p>
<p class="text-sm">Zustand: ${ac.condition}%</p>
</div>
<button class="btn btn-success" onclick="executeFlightForAircraft(${ac.id})">
▶️ Flug starten!
</button>
</div>
</div>
`;
}
return '';
}).join('');
list.innerHTML = aircraftHtml;
openModal('flightModal');
}
async function executeFlightForAircraft(aircraftId) {
// Einfach die erste Route verwenden für Demo
if (GAME_DATA.routes.length > 0) {
closeModal('flightModal');
try {
const result = await api.post('game.php?action=flight', {
route_id: GAME_DATA.routes[0].id,
aircraft_id: aircraftId
});
if (result.success) {
showToast(`Flug erfolgreich! +${formatMoney(result.profit)}`, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showToast(result.error || 'Flug fehlgeschlagen', 'error');
}
} catch (error) {
showToast('Netzwerkfehler', 'error');
}
} else {
showToast('Keine Route verfügbar!', 'warning');
}
}
function purchaseAircraft(type) {
api.post('game.php?action=purchase', { aircraft_type: type })
.then(result => {
if (result.success) {
showToast(`${type} gekauft für ${formatMoney(result.price)}`, 'success');
closeModal('purchaseAircraftModal');
setTimeout(() => location.reload(), 1500);
} else {
showToast(result.error || 'Kauf fehlgeschlagen', 'error');
}
});
}
function updateDashboard() {
// Dashboard aktualisieren (wird bei Bedarf erweitert)
location.reload();
}
</script>
<script src="<?= BASE_PATH ?>assets/js/main.js"></script>
</body>
</html>

203
www/public/index.php Normal file
View File

@@ -0,0 +1,203 @@
<?php
/**
* Airline Tycoon - Landing Page
* Comic-Style mit Dark Mode Support
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
// Redirect wenn bereits eingeloggt
if (Session::isLoggedIn()) {
redirect('/dashboard.php');
}
?>
<!DOCTYPE html>
<html lang="de" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Airline Tycoon - Baue dein Flugimperium im Browser auf!">
<meta name="csrf-token" content="<?= h(csrf_token()) ?>">
<title>Airline Tycoon - Werde zum Flugmagnat!</title>
<link rel="stylesheet" href="<?= BASE_PATH ?>assets/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✈️</text></svg>">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="navbar-container">
<a href="<?= BASE_PATH ?>" class="navbar-brand">
✈️ Airline Tycoon
</a>
<button class="navbar-toggle" aria-label="Menü">☰</button>
<ul class="navbar-menu">
<li><a href="#features" class="navbar-item">Features</a></li>
<li><a href="#screenshots" class="navbar-item">Screenshots</a></li>
<li><a href="<?= BASE_PATH ?>login.php" class="navbar-item">Anmelden</a></li>
<li><a href="<?= BASE_PATH ?>register.php" class="btn btn-primary btn-sm">Kostenlos spielen</a></li>
<li>
<button class="theme-toggle" id="themeToggle" aria-label="Design wechseln">🌙</button>
</li>
</ul>
</div>
</nav>
<!-- Hero Section -->
<section class="hero">
<div class="hero-content">
<h1 class="hero-title">Werde zum Flugmagnat!</h1>
<p class="hero-subtitle">
Baue deine eigene Airline auf, verbinde Städte auf der ganzen Welt
und werde zur Nummer 1 in diesem kostenlosen Browser-MMO!
</p>
<div class="hero-cta">
<a href="<?= BASE_PATH ?>register.php" class="btn btn-primary btn-lg">
🎮 Kostenlos spielen
</a>
<a href="#features" class="btn btn-secondary btn-lg">
Mehr erfahren
</a>
</div>
<!-- Comic Sprechblase -->
<div class="comic-bubble mt-xl">
<p class="text-center">
<strong>💡 Tipp:</strong> Starte mit kurzen Strecken, spare Geld und kaufe größere Flugzeuge,
um noch mehr Passagiere zu transportieren!
</p>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="p-lg">
<div class="panel">
<h2 class="panel-header">🎮 Spielfeatures</h2>
<div class="stats-grid mt-lg">
<div class="card">
<div class="card-icon">🛫</div>
<h3 class="mt-md">60+ Flughäfen</h3>
<p>Verbinde Städte auf allen Kontinenten und erweitere dein Streckennetz!</p>
</div>
<div class="card">
<div class="card-icon">✈️</div>
<h3 class="mt-md">5 Flugzeugtypen</h3>
<p>Vom kleinen Dash 8 bis zum riesigen Airbus A380 - wähle das richtige Flugzeug!</p>
</div>
<div class="card">
<div class="card-icon">💰</div>
<h3 class="mt-md">Wirtschaftssimulation</h3>
<p>Verwalte dein Budget, maximiere Gewinne und werde reich!</p>
</div>
<div class="card">
<div class="card-icon">🏆</div>
<h3 class="mt-md">Rangliste</h3>
<p>Konkurriere mit anderen Spielern um den Spitzenplatz!</p>
</div>
<div class="card">
<div class="card-icon">📱</div>
<h3 class="mt-md">Mobile optimiert</h3>
<p>Spiele überall - am Computer, Tablet oder Smartphone!</p>
</div>
<div class="card">
<div class="card-icon">🌙</div>
<h3 class="mt-md">Dark Mode</h3>
<p>Wähle zwischen hellem und dunklem Design - wie es dir gefällt!</p>
</div>
</div>
</div>
</section>
<!-- Screenshots Section -->
<section id="screenshots" class="p-lg">
<div class="panel">
<h2 class="panel-header">🖥️ Spielansichten</h2>
<div class="d-grid" style="grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-lg);">
<div class="card">
<h3>📊 Dashboard</h3>
<p>Behalte deine Finanzen, Flugzeuge und Routes im Blick!</p>
</div>
<div class="card">
<h3>🌍 Weltkarte</h3>
<p>Interaktive Karte mit allen Flughäfen. Tippe zum Auswählen!</p>
</div>
<div class="card">
<h3>✈️ Flottenverwaltung</h3>
<p>Verwalte deine Flugzeuge und behalte deren Zustand im Auge!</p>
</div>
</div>
</div>
</section>
<!-- How to Play -->
<section class="p-lg">
<div class="panel">
<h2 class="panel-header">🎯 So spielst du</h2>
<div class="d-flex flex-wrap gap-lg mt-lg" style="justify-content: center;">
<div class="text-center" style="max-width: 200px;">
<div class="badge">1</div>
<h4 class="mt-sm">Registrieren</h4>
<p>Kostenloses Konto erstellen</p>
</div>
<div class="text-center" style="max-width: 200px;">
<div class="badge">2</div>
<h4 class="mt-sm">Flugzeuge kaufen</h4>
<p>Starte mit deinem ersten Flieger</p>
</div>
<div class="text-center" style="max-width: 200px;">
<div class="badge">3</div>
<h4 class="mt-sm">Routen erstellen</h4>
<p>Verbinde Flughäfen weltweit</p>
</div>
<div class="text-center" style="max-width: 200px;">
<div class="badge">4</div>
<h4 class="mt-sm">Fliegen!</h4>
<p>Verdiene Geld und Level auf</p>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="p-lg text-center">
<div class="panel" style="max-width: 600px; margin: 0 auto;">
<h2>Bereit abzuheben?</h2>
<p class="mt-md mb-lg">
Starte jetzt dein Flugabenteuer! Keine Kosten, keine Downloads -
einfach Browser öffnen und spielen!
</p>
<a href="<?= BASE_PATH ?>register.php" class="btn btn-primary btn-lg">
🚀 Jetzt kostenlos spielen!
</a>
</div>
</section>
<!-- Footer -->
<footer class="p-lg text-center" style="border-top: 3px solid var(--text-primary); margin-top: var(--space-xl);">
<p>
<strong>Airline Tycoon</strong> - Ein kostenloses Browser-MMO
</p>
<p class="mt-sm text-muted">
© 2024 Airline Tycoon | Alle Rechte vorbehalten
</p>
</footer>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
<script src="<?= BASE_PATH ?>assets/js/main.js"></script>
</body>
</html>

134
www/public/login.php Normal file
View File

@@ -0,0 +1,134 @@
<?php
/**
* Airline Tycoon - Login Page
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
$error = '';
$success = Session::flash('success');
// Login verarbeiten
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']);
if (!validate_csrf($_POST['csrf_token'] ?? '')) {
$error = 'Ungültige Anfrage. Bitte versuche es erneut.';
} elseif (empty($username) || empty($password)) {
$error = 'Bitte fülle alle Felder aus.';
} else {
$user = $db->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');
}
?>
<!DOCTYPE html>
<html lang="de" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anmelden - Airline Tycoon</title>
<meta name="csrf-token" content="<?= h(csrf_token()) ?>">
<link rel="stylesheet" href="<?= BASE_PATH ?>assets/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✈️</text></svg>">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="navbar-container">
<a href="<?= BASE_PATH ?>" class="navbar-brand">✈️ Airline Tycoon</a>
<ul class="navbar-menu">
<li><a href="<?= BASE_PATH ?>" class="navbar-item">Zurück zur Startseite</a></li>
</ul>
</div>
</nav>
<main class="container p-lg">
<div class="panel" style="max-width: 450px; margin: 0 auto;">
<h1 class="text-center mb-lg">🔐 Anmelden</h1>
<?php if ($success): ?>
<div class="card mb-md" style="background: rgba(107, 203, 119, 0.2); border-color: var(--comic-green);">
<p class="text-success">✅ <?= h($success) ?></p>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="card mb-md" style="background: rgba(255, 107, 107, 0.2); border-color: var(--comic-red);">
<p class="text-danger">❌ <?= h($error) ?></p>
</div>
<?php endif; ?>
<form method="POST" action="<?= BASE_PATH ?>login.php" data-ajax>
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<div class="form-group">
<label class="form-label" for="username">Benutzername</label>
<input
type="text"
id="username"
name="username"
class="form-input"
placeholder="Dein Benutzername"
required
autocomplete="username"
value="<?= h($_POST['username'] ?? '') ?>"
>
</div>
<div class="form-group">
<label class="form-label" for="password">Passwort</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Dein Passwort"
required
autocomplete="current-password"
>
</div>
<div class="form-group">
<label class="d-flex flex-center gap-sm">
<input type="checkbox" name="remember" id="remember">
<span>Angemeldet bleiben</span>
</label>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">
🚀 Anmelden
</button>
</form>
<div class="text-center mt-lg">
<p>Noch kein Konto?</p>
<a href="<?= BASE_PATH ?>register.php" class="btn btn-secondary">
Jetzt registrieren
</a>
</div>
</div>
</main>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
<script src="<?= BASE_PATH ?>assets/js/main.js"></script>
</body>
</html>

11
www/public/logout.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
/**
* Airline Tycoon - Logout
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
Session::logout();
Session::flash('success', 'Du hast dich erfolgreich abgemeldet.');
redirect('/login.php');

207
www/public/register.php Normal file
View File

@@ -0,0 +1,207 @@
<?php
/**
* Airline Tycoon - Registration Page
*/
require_once dirname(__DIR__) . '/src/bootstrap.php';
$error = '';
// Registration verarbeiten
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
$airline_name = trim($_POST['airline_name'] ?? '');
if (!validate_csrf($_POST['csrf_token'] ?? '')) {
$error = 'Ungültige Anfrage. Bitte versuche es erneut.';
} elseif (empty($username) || empty($email) || empty($password)) {
$error = 'Bitte fülle alle Pflichtfelder aus.';
} elseif (strlen($username) < 3 || strlen($username) > 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');
}
?>
<!DOCTYPE html>
<html lang="de" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrieren - Airline Tycoon</title>
<meta name="csrf-token" content="<?= h(csrf_token()) ?>">
<link rel="stylesheet" href="<?= BASE_PATH ?>assets/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✈️</text></svg>">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="navbar-container">
<a href="<?= BASE_PATH ?>" class="navbar-brand">✈️ Airline Tycoon</a>
<ul class="navbar-menu">
<li><a href="<?= BASE_PATH ?>" class="navbar-item">Zurück zur Startseite</a></li>
</ul>
</div>
</nav>
<main class="container p-lg">
<div class="panel" style="max-width: 500px; margin: 0 auto;">
<h1 class="text-center mb-lg">📝 Registrieren</h1>
<?php if ($error): ?>
<div class="card mb-md" style="background: rgba(255, 107, 107, 0.2); border-color: var(--comic-red);">
<p class="text-danger">❌ <?= h($error) ?></p>
</div>
<?php endif; ?>
<form method="POST" action="<?= BASE_PATH ?>register.php" data-ajax>
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<div class="form-group">
<label class="form-label" for="username">Benutzername *</label>
<input
type="text"
id="username"
name="username"
class="form-input"
placeholder="Dein Benutzername"
required
minlength="3"
maxlength="50"
pattern="[a-zA-Z0-9_]+"
autocomplete="username"
value="<?= h($_POST['username'] ?? '') ?>"
>
<p class="form-help">3-50 Zeichen, nur Buchstaben, Zahlen und Unterstriche</p>
</div>
<div class="form-group">
<label class="form-label" for="email">E-Mail *</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="deine@email.de"
required
autocomplete="email"
value="<?= h($_POST['email'] ?? '') ?>"
>
</div>
<div class="form-group">
<label class="form-label" for="airline_name">Airline-Name</label>
<input
type="text"
id="airline_name"
name="airline_name"
class="form-input"
placeholder="z.B. German Wings Express"
maxlength="100"
value="<?= h($_POST['airline_name'] ?? '') ?>"
>
<p class="form-help">Leer lassen für Standard-Name</p>
</div>
<div class="form-group">
<label class="form-label" for="password">Passwort *</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Mindestens 8 Zeichen"
required
minlength="8"
autocomplete="new-password"
>
</div>
<div class="form-group">
<label class="form-label" for="password_confirm">Passwort bestätigen *</label>
<input
type="password"
id="password_confirm"
name="password_confirm"
class="form-input"
placeholder="Passwort wiederholen"
required
autocomplete="new-password"
>
</div>
<div class="form-group">
<label class="d-flex flex-center gap-sm">
<input type="checkbox" name="terms" id="terms" required>
<span>Ich akzeptiere die <a href="#" target="_blank">Nutzungsbedingungen</a></span>
</label>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">
✈️ Kostenlos registrieren
</button>
</form>
<div class="text-center mt-lg">
<p>Bereits ein Konto?</p>
<a href="<?= BASE_PATH ?>login.php" class="btn btn-secondary">
Jetzt anmelden
</a>
</div>
<!-- Comic Feature Box -->
<div class="comic-bubble mt-lg">
<p class="text-center">
🎁 <strong>Willkommensbonus:</strong> Starte mit 500.000 € und einem Dash 8-100 Flugzeug!
</p>
</div>
</div>
</main>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
<script src="<?= BASE_PATH ?>assets/js/main.js"></script>
</body>
</html>

373
www/src/Database.php Normal file
View File

@@ -0,0 +1,373 @@
<?php
/**
* Airline Tycoon - Database Klasse
*
* SQLite Datenbankverbindung und Helper-Methoden
*/
class Database {
private static ?Database $instance = null;
private PDO $connection;
private array $config;
private function __construct() {
$this->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;
}
}

78
www/src/Router.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
/**
* Airline Tycoon - Router
*
* Einfacher URL-Router für das Spiel
*/
class Router {
private array $routes = [];
private string $basePath;
public function __construct(string $basePath = '') {
$this->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;
}
}

121
www/src/Session.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
/**
* Airline Tycoon - Session Verwaltung
*/
class Session {
private static bool $started = false;
public static function start(): void {
if (self::$started) {
return;
}
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_samesite', 'Strict');
session_start();
self::$started = true;
// Session-Check für XSS-Schutz
if (!isset($_SESSION['user_agent']) || $_SESSION['user_agent'] !== md5($_SERVER['HTTP_USER_AGENT'] ?? '')) {
self::regenerate();
}
}
public static function regenerate(): void {
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
}
$_SESSION['user_agent'] = md5($_SERVER['HTTP_USER_AGENT'] ?? '');
$_SESSION['created'] = time();
}
public static function set(string $key, mixed $value): void {
$_SESSION[$key] = $value;
}
public static function get(string $key, mixed $default = null): mixed {
return $_SESSION[$key] ?? $default;
}
public static function has(string $key): bool {
return isset($_SESSION[$key]);
}
public static function remove(string $key): void {
unset($_SESSION[$key]);
}
public static function destroy(): void {
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION = [];
session_destroy();
}
self::$started = false;
}
public static function isLoggedIn(): bool {
return self::has('user_id') && self::has('authenticated') && self::get('authenticated') === true;
}
public static function getUserId(): ?int {
return self::get('user_id') ? (int) self::get('user_id') : null;
}
public static function setUserId(int $id): void {
self::set('user_id', $id);
self::set('authenticated', true);
}
public static function login(array $user): void {
self::regenerate();
self::setUserId($user['id']);
self::set('username', $user['username']);
self::set('airline_name', $user['airline_name']);
self::set('balance', $user['balance']);
self::set('level', $user['level']);
self::set('login_time', time());
}
public static function logout(): void {
self::destroy();
}
public static function refreshUserData(Database $db): void {
$userId = self::getUserId();
if (!$userId) {
return;
}
$user = $db->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;
}
}

122
www/src/bootstrap.php Normal file
View File

@@ -0,0 +1,122 @@
<?php
/**
* Airline Tycoon - Bootstrap
*
* Initialisiert die Anwendung
*/
// Fehlerbehandlung
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
// Zeitzone setzen
date_default_timezone_set('Europe/Berlin');
// Base Path ermitteln
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
$basePath = str_replace('/www/public', '', $scriptPath);
if ($basePath === $scriptPath) {
$basePath = '/';
}
define('BASE_PATH', $basePath);
// Autoloader für Klassen
spl_autoload_register(function ($class) {
$paths = [
dirname(__DIR__) . '/src/' . $class . '.php',
dirname(__DIR__) . '/www/public/api/' . $class . '.php'
];
foreach ($paths as $path) {
if (file_exists($path)) {
require_once $path;
return;
}
}
});
// Konfiguration laden
$config = require dirname(__DIR__) . '/src/config.php';
// Datenbank Verbindung
$db = Database::getInstance();
// Session starten
Session::start();
// Helper Functions
function h(string $str): string {
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
function redirect(string $path): void {
header('Location: ' . BASE_PATH . $path);
exit;
}
function jsonResponse(array $data, int $status = 200): void {
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function isAjax(): bool {
return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
function formatMoney(float $amount): string {
return number_format($amount, 2, ',', '.') . ' €';
}
function formatNumber(int $num): string {
return number_format($num, 0, ',', '.');
}
function timeAgo(string $datetime): string {
$time = strtotime($datetime);
$diff = time() - $time;
if ($diff < 60) {
return 'vor ' . $diff . ' Sekunden';
}
if ($diff < 3600) {
return 'vor ' . floor($diff / 60) . ' Minuten';
}
if ($diff < 86400) {
return 'vor ' . floor($diff / 3600) . ' Stunden';
}
if ($diff < 2592000) {
return 'vor ' . floor($diff / 86400) . ' Tagen';
}
return date('d.m.Y H:i', $time);
}
// CSRF-Token für Formulare
function csrf_token(): string {
return Session::getCsrfToken();
}
function csrf_field(): string {
return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
}
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
}

89
www/src/config.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
/**
* Airline Tycoon - Konfiguration
*
* Zentrale Konfigurationsdatei für das Spiel
*/
return [
// Datenbank
'database' => [
'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'
]
];