/** * Airline Tycoon - Main JavaScript * Vanilla JS, Mobile-responsive, Touch-friendly */ (function() { 'use strict'; // ============================================ // GLOBAL STATE // ============================================ const App = { config: { basePath: window.BASE_PATH || '/', apiTimeout: 10000 }, state: { theme: localStorage.getItem('theme') || 'light', map: null, markers: [], selectedAirport: null } }; // ============================================ // INITIALIZATION // ============================================ document.addEventListener('DOMContentLoaded', function() { initTheme(); initNavigation(); initModals(); initToasts(); initForms(); initWorldMap(); initTouchHandlers(); }); // ============================================ // THEME MANAGEMENT // ============================================ function initTheme() { const themeToggle = document.getElementById('themeToggle'); // System-Präferenz prüfen if (!localStorage.getItem('theme')) { const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; App.state.theme = prefersDark ? 'dark' : 'light'; } applyTheme(App.state.theme); if (themeToggle) { themeToggle.addEventListener('click', toggleTheme); } } function toggleTheme() { App.state.theme = App.state.theme === 'light' ? 'dark' : 'light'; localStorage.setItem('theme', App.state.theme); applyTheme(App.state.theme); } function applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme); const themeIcon = document.querySelector('#themeToggle, .theme-toggle'); if (themeIcon) { themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙'; } } // ============================================ // NAVIGATION // ============================================ function initNavigation() { const navbarToggle = document.querySelector('.navbar-toggle'); const navbarMenu = document.querySelector('.navbar-menu'); const navbarOverlay = document.querySelector('.navbar-overlay'); if (navbarToggle && navbarMenu) { navbarToggle.addEventListener('click', function() { navbarMenu.classList.toggle('active'); navbarOverlay?.classList.toggle('active'); }); navbarOverlay?.addEventListener('click', function() { navbarMenu.classList.remove('active'); navbarOverlay?.classList.remove('active'); }); } // Aktive Navigation highlight const currentPath = window.location.pathname; document.querySelectorAll('.navbar-item').forEach(item => { if (item.getAttribute('href') === currentPath) { item.classList.add('active'); } }); } // ============================================ // MODALS // ============================================ function initModals() { document.querySelectorAll('[data-modal]').forEach(trigger => { trigger.addEventListener('click', function(e) { e.preventDefault(); const modalId = this.getAttribute('data-modal'); openModal(modalId); }); }); document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', function(e) { if (e.target === this) { closeModal(this.id); } }); }); document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', function() { const modal = this.closest('.modal-overlay'); if (modal) { closeModal(modal.id); } }); }); // ESC Key zum Schließen document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay.active').forEach(modal => { closeModal(modal.id); }); } }); } function openModal(id) { const modal = document.getElementById(id); if (modal) { modal.classList.add('active'); document.body.style.overflow = 'hidden'; } } function closeModal(id) { const modal = document.getElementById(id); if (modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } } // ============================================ // TOAST NOTIFICATIONS // ============================================ function initToasts() { // Toasts werden dynamisch erstellt } window.showToast = function(message, type = 'info', duration = 4000) { const container = document.getElementById('toastContainer') || createToastContainer(); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.innerHTML = ` ${getToastIcon(type)} ${escapeHtml(message)} `; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, duration); }; function createToastContainer() { const container = document.createElement('div'); container.id = 'toastContainer'; container.className = 'toast-container'; document.body.appendChild(container); return container; } function getToastIcon(type) { const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; return icons[type] || icons.info; } // ============================================ // FORM HANDLING // ============================================ function initForms() { document.querySelectorAll('form[data-ajax]').forEach(form => { form.addEventListener('submit', handleAjaxForm); }); document.querySelectorAll('form').forEach(form => { // CSRF Token hinzufügen const csrfInput = form.querySelector('input[name="csrf_token"]'); if (!csrfInput && form.dataset.csrf !== 'false') { const token = document.querySelector('meta[name="csrf-token"]'); if (token) { const input = document.createElement('input'); input.type = 'hidden'; input.name = 'csrf_token'; input.value = token.content; form.appendChild(input); } } // Form Validation form.addEventListener('submit', function(e) { if (!validateForm(this)) { e.preventDefault(); } }); }); } async function handleAjaxForm(e) { e.preventDefault(); const form = e.target; const submitBtn = form.querySelector('[type="submit"]'); const originalText = submitBtn?.textContent; try { if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = ''; } const formData = new FormData(form); const response = await fetch(form.action || window.location.href, { method: form.method || 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const data = await response.json(); if (response.ok) { showToast(data.message || 'Erfolgreich!', 'success'); if (data.redirect) { setTimeout(() => { window.location.href = data.redirect; }, 1000); } else if (data.reset && form.dataset.reset !== 'false') { form.reset(); } // Callback if (typeof window[data.onSuccess] === 'function') { window[data.onSuccess](data); } } else { showToast(data.error || 'Ein Fehler ist aufgetreten', 'error'); } } catch (error) { console.error('Form submission error:', error); showToast('Netzwerkfehler. Bitte versuche es erneut.', 'error'); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; } } } function validateForm(form) { let isValid = true; form.querySelectorAll('[required]').forEach(field => { if (!field.value.trim()) { isValid = false; showFieldError(field, 'Dieses Feld ist erforderlich'); } else { clearFieldError(field); } }); // Email Validation form.querySelectorAll('[type="email"]').forEach(field => { if (field.value && !isValidEmail(field.value)) { isValid = false; showFieldError(field, 'Bitte gib eine gültige E-Mail ein'); } }); return isValid; } function showFieldError(field, message) { clearFieldError(field); field.classList.add('error'); const error = document.createElement('div'); error.className = 'form-error'; error.textContent = message; field.parentNode.appendChild(error); } function clearFieldError(field) { field.classList.remove('error'); const error = field.parentNode.querySelector('.form-error'); if (error) { error.remove(); } } function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } // ============================================ // WORLD MAP // ============================================ function initWorldMap() { const mapContainer = document.getElementById('mapContainer'); if (!mapContainer) return; // Map Canvas erstellen const canvas = document.createElement('canvas'); canvas.id = 'mapCanvas'; canvas.className = 'map-canvas'; mapContainer.appendChild(canvas); // Controls erstellen const controls = document.createElement('div'); controls.className = 'map-controls'; controls.innerHTML = ` `; mapContainer.appendChild(controls); // Tooltip erstellen const tooltip = document.createElement('div'); tooltip.className = 'map-tooltip'; tooltip.id = 'mapTooltip'; mapContainer.appendChild(tooltip); // Map State App.state.map = { canvas: canvas, ctx: canvas.getContext('2d'), zoom: 1, offsetX: 0, offsetY: 0, isDragging: false, lastX: 0, lastY: 0 }; // Canvas Größe setzen resizeMapCanvas(); window.addEventListener('resize', resizeMapCanvas); // Map Events canvas.addEventListener('mousedown', mapMouseDown); canvas.addEventListener('mousemove', mapMouseMove); canvas.addEventListener('mouseup', mapMouseUp); canvas.addEventListener('mouseleave', mapMouseUp); canvas.addEventListener('wheel', mapWheel); // Touch Events canvas.addEventListener('touchstart', mapTouchStart, { passive: false }); canvas.addEventListener('touchmove', mapTouchMove, { passive: false }); canvas.addEventListener('touchend', mapTouchEnd); // Control Events document.getElementById('mapZoomIn')?.addEventListener('click', () => mapZoom(1.2)); document.getElementById('mapZoomOut')?.addEventListener('click', () => mapZoom(0.8)); document.getElementById('mapReset')?.addEventListener('click', mapReset); // Map laden loadMapData(); } function resizeMapCanvas() { if (!App.state.map) return; const rect = App.state.map.canvas.parentElement.getBoundingClientRect(); App.state.map.canvas.width = rect.width; App.state.map.canvas.height = rect.height; renderMap(); } async function loadMapData() { try { const response = await fetch(App.config.basePath + 'api/airports.php?action=list'); const data = await response.json(); if (data.airports) { App.state.airports = data.airports; renderMap(); } } catch (error) { console.error('Map data loading error:', error); } } function renderMap() { if (!App.state.map || !App.state.map.ctx) return; const { canvas, ctx, zoom, offsetX, offsetY } = App.state.map; // Canvas löschen ctx.fillStyle = getComputedStyle(document.documentElement) .getPropertyValue('--bg-tertiary').trim() || '#e8e0d4'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Weltkarte zeichnen (vereinfacht) drawWorldMap(ctx, canvas.width, canvas.height, zoom, offsetX, offsetY); // Flughäfen zeichnen if (App.state.airports) { App.state.airports.forEach(airport => { drawAirport(ctx, airport, zoom, offsetX, offsetY); }); } } function drawWorldMap(ctx, width, height, zoom, offsetX, offsetY) { // Vereinfachte Weltkarte mit Kontinenten ctx.save(); ctx.translate(width / 2 + offsetX, height / 2 + offsetY); ctx.scale(zoom, zoom); // Ozean Hintergrund ctx.fillStyle = 'rgba(78, 205, 196, 0.1)'; ctx.beginPath(); ctx.ellipse(0, 0, 800, 400, 0, 0, Math.PI * 2); ctx.fill(); // Vereinfachte Kontinente const continents = [ // Nordamerika { x: -300, y: -100, points: [[0,0], [100,20], [150,80], [100,150], [0,120], [-50,50]], color: '#6BCB77' }, // Südamerika { x: -100, y: 100, points: [[0,0], [50,20], [60,80], [30,150], [-20,100], [-30,40]], color: '#6BCB77' }, // Europa { x: 100, y: -80, points: [[0,0], [80,10], [100,50], [60,80], [10,60], [-20,20]], color: '#FFD93D' }, // Afrika { x: 120, y: 60, points: [[0,0], [70,10], [90,60], [50,120], [0,80], [-30,30]], color: '#FFD93D' }, // Asien { x: 350, y: -50, points: [[0,0], [150,20], [200,80], [180,150], [100,100], [30,30]], color: '#FF9F43' }, // Australien { x: 450, y: 150, points: [[0,0], [80,20], [90,70], [50,100], [-10,60], [-30,20]], color: '#FF6B6B' } ]; continents.forEach(continent => { ctx.fillStyle = continent.color + '40'; ctx.strokeStyle = continent.color; ctx.lineWidth = 2 / zoom; ctx.beginPath(); ctx.moveTo(continent.x + continent.points[0][0], continent.y + continent.points[0][1]); continent.points.forEach((point, i) => { if (i > 0) ctx.lineTo(continent.x + point[0], continent.y + point[1]); }); ctx.closePath(); ctx.fill(); ctx.stroke(); }); ctx.restore(); } function drawAirport(ctx, airport, zoom, offsetX, offsetY) { const { canvas } = App.state.map; // Koordinaten zu Pixeln konvertieren const x = canvas.width / 2 + offsetX + (airport.longitude * 8 * zoom); const y = canvas.height / 2 + offsetY + (airport.latitude * -5 * zoom); // Sichtbarkeit prüfen if (x < -20 || x > canvas.width + 20 || y < -20 || y > canvas.height + 20) return; ctx.save(); // Airport Marker const isSelected = App.state.selectedAirport?.id === airport.id; const radius = (isSelected ? 12 : 8) / zoom; // Glow für ausgewählten Flughafen if (isSelected) { ctx.fillStyle = 'rgba(255, 107, 107, 0.3)'; ctx.beginPath(); ctx.arc(x, y, radius * 2, 0, Math.PI * 2); ctx.fill(); } // Hauptmarker ctx.fillStyle = isSelected ? '#FF6B6B' : '#4ECDC4'; ctx.strokeStyle = '#2c2416'; ctx.lineWidth = 2 / zoom; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); // IATA Code ctx.fillStyle = '#2c2416'; ctx.font = `${10 / zoom}px Nunito, sans-serif`; ctx.textAlign = 'center'; ctx.fillText(airport.iata_code, x, y + radius + 14 / zoom); ctx.restore(); // Airport in Marker-Array speichern airport.pixelX = x; airport.pixelY = y; } function getAirportFromPixel(x, y) { if (!App.state.airports) return null; return App.state.airports.find(airport => { const dx = Math.abs(airport.pixelX - x); const dy = Math.abs(airport.pixelY - y); return dx < 20 && dy < 20; }); } function mapMouseDown(e) { const rect = App.state.map.canvas.getBoundingClientRect(); App.state.map.isDragging = true; App.state.map.lastX = e.clientX - rect.left; App.state.map.lastY = e.clientY - rect.top; App.state.map.dragStartTime = Date.now(); App.state.map.dragStartX = App.state.map.offsetX; App.state.map.dragStartY = App.state.map.offsetY; } function mapMouseMove(e) { const rect = App.state.map.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (App.state.map.isDragging) { const dx = x - App.state.map.lastX; const dy = y - App.state.map.lastY; App.state.map.offsetX += dx; App.state.map.offsetY += dy; App.state.map.lastX = x; App.state.map.lastY = y; renderMap(); } else { // Tooltip bei Hover const airport = getAirportFromPixel(x, y); const tooltip = document.getElementById('mapTooltip'); if (airport) { tooltip.innerHTML = ` ${airport.iata_code} - ${airport.name}
${airport.city}, ${airport.country}
Passagiere: ${formatNumber(airport.passenger_volume)}/Jahr `; tooltip.style.left = (x + 15) + 'px'; tooltip.style.top = (y - 10) + 'px'; tooltip.classList.add('visible'); App.state.map.canvas.style.cursor = 'pointer'; } else { tooltip.classList.remove('visible'); App.state.map.canvas.style.cursor = 'grab'; } } } function mapMouseUp(e) { if (App.state.map.isDragging) { const dragDuration = Date.now() - App.state.map.dragStartTime; const dragDistance = Math.abs(App.state.map.offsetX - App.state.map.dragStartX) + Math.abs(App.state.map.offsetY - App.state.map.dragStartY); // Klick erkennen (kurze Dauer, little movement) if (dragDuration < 200 && dragDistance < 5) { const rect = App.state.map.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const airport = getAirportFromPixel(x, y); if (airport) { selectAirport(airport); } } } App.state.map.isDragging = false; } function mapWheel(e) { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; mapZoom(delta); } function mapTouchStart(e) { e.preventDefault(); if (e.touches.length === 1) { const touch = e.touches[0]; const rect = App.state.map.canvas.getBoundingClientRect(); App.state.map.isDragging = true; App.state.map.lastX = touch.clientX - rect.left; App.state.map.lastY = touch.clientY - rect.top; App.state.map.dragStartTime = Date.now(); App.state.map.dragStartX = App.state.map.offsetX; App.state.map.dragStartY = App.state.map.offsetY; App.state.map.touchStartX = touch.clientX; App.state.map.touchStartY = touch.clientY; } } function mapTouchMove(e) { e.preventDefault(); if (e.touches.length === 1 && App.state.map.isDragging) { const touch = e.touches[0]; const rect = App.state.map.canvas.getBoundingClientRect(); const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; const dx = x - App.state.map.lastX; const dy = y - App.state.map.lastY; App.state.map.offsetX += dx; App.state.map.offsetY += dy; App.state.map.lastX = x; App.state.map.lastY = y; renderMap(); } } function mapTouchEnd(e) { if (App.state.map.isDragging) { const touch = e.changedTouches[0]; const dragDuration = Date.now() - App.state.map.dragStartTime; const dragDistance = Math.abs(touch.clientX - App.state.map.touchStartX) + Math.abs(touch.clientY - App.state.map.touchStartY); // Tap erkennen if (dragDuration < 200 && dragDistance < 10) { const rect = App.state.map.canvas.getBoundingClientRect(); const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; const airport = getAirportFromPixel(x, y); if (airport) { selectAirport(airport); } } } App.state.map.isDragging = false; } function mapZoom(factor) { const newZoom = Math.max(0.5, Math.min(4, App.state.map.zoom * factor)); // Zoom um Mausposition const rect = App.state.map.canvas.getBoundingClientRect(); const mouseX = App.state.map.lastX - rect.width / 2; const mouseY = App.state.map.lastY - rect.height / 2; App.state.map.offsetX = mouseX - (mouseX - App.state.map.offsetX) * factor; App.state.map.offsetY = mouseY - (mouseY - App.state.map.offsetY) * factor; App.state.map.zoom = newZoom; renderMap(); } function mapReset() { App.state.map.zoom = 1; App.state.map.offsetX = 0; App.state.map.offsetY = 0; renderMap(); } function selectAirport(airport) { App.state.selectedAirport = airport; renderMap(); // Airport Info Panel aktualisieren const infoPanel = document.getElementById('airportInfo'); if (infoPanel && airport) { infoPanel.innerHTML = `

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

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

Kontinent: ${airport.continent}

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

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

`; infoPanel.classList.add('active'); } // Event emit document.dispatchEvent(new CustomEvent('airportSelected', { detail: airport })); } // ============================================ // TOUCH HANDLERS // ============================================ function initTouchHandlers() { // Touch-freundliche Button-Press-Effekte document.querySelectorAll('.btn').forEach(btn => { btn.addEventListener('touchstart', function() { this.classList.add('pressed'); }, { passive: true }); btn.addEventListener('touchend', function() { this.classList.remove('pressed'); }, { passive: true }); }); } // ============================================ // API HELPERS // ============================================ window.api = { async get(endpoint, params = {}) { const queryString = new URLSearchParams(params).toString(); const url = App.config.basePath + 'api/' + endpoint + (queryString ? '?' + queryString : ''); const response = await fetch(url, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); return response.json(); }, async post(endpoint, data = {}) { const formData = new FormData(); Object.keys(data).forEach(key => formData.append(key, data[key])); const response = await fetch(App.config.basePath + 'api/' + endpoint, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); return response.json(); } }; // ============================================ // GAME ACTIONS // ============================================ window.createRouteFromAirport = async function(fromAirportId) { const modal = document.getElementById('createRouteModal'); if (modal) { document.getElementById('routeFromAirport').value = fromAirportId; openModal('createRouteModal'); } }; window.executeFlight = async function(routeId, aircraftId) { try { const result = await api.post('game.php?action=flight', { route_id: routeId, aircraft_id: aircraftId }); if (result.success) { showToast(`Flug erfolgreich! +${formatMoney(result.profit)}`, 'success'); // Dashboard aktualisieren if (typeof updateDashboard === 'function') { updateDashboard(); } } else { showToast(result.error || 'Flug fehlgeschlagen', 'error'); } } catch (error) { showToast('Netzwerkfehler', 'error'); } }; window.purchaseAircraft = async function(type) { try { const result = await api.post('game.php?action=purchase', { aircraft_type: type }); if (result.success) { showToast(`${type} gekauft für ${formatMoney(result.price)}`, 'success'); // Modal schließen const modal = document.getElementById('purchaseAircraftModal'); if (modal) closeModal('purchaseAircraftModal'); // Dashboard aktualisieren if (typeof updateDashboard === 'function') { updateDashboard(); } } else { showToast(result.error || 'Kauf fehlgeschlagen', 'error'); } } catch (error) { showToast('Netzwerkfehler', 'error'); } }; // ============================================ // UTILITIES // ============================================ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatNumber(num) { return new Intl.NumberFormat('de-DE').format(num); } function formatMoney(amount) { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(amount); } // ============================================ // EXPORTS // ============================================ window.App = App; window.showToast = showToast; window.openModal = openModal; window.closeModal = closeModal; })();