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

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>