feat(phase1): complete base structure - landing page, auth, dashboard, airports, API, DB schema, 60 airports CSV
This commit is contained in:
116
database/schema.sql
Normal file
116
database/schema.sql
Normal 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
61
www/data/airports.csv
Normal 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
|
||||
|
373
www/public/airports.php
Normal file
373
www/public/airports.php
Normal 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
142
www/public/api/airports.php
Normal 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
189
www/public/api/auth.php
Normal 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
331
www/public/api/game.php
Normal 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)
|
||||
]);
|
||||
}
|
||||
1050
www/public/assets/css/style.css
Normal file
1050
www/public/assets/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
865
www/public/assets/js/main.js
Normal file
865
www/public/assets/js/main.js
Normal 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
461
www/public/dashboard.php
Normal 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
203
www/public/index.php
Normal 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
134
www/public/login.php
Normal 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
11
www/public/logout.php
Normal 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
207
www/public/register.php
Normal 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
373
www/src/Database.php
Normal 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
78
www/src/Router.php
Normal 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
121
www/src/Session.php
Normal 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
122
www/src/bootstrap.php
Normal 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
89
www/src/config.php
Normal 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'
|
||||
]
|
||||
];
|
||||
Reference in New Issue
Block a user