Files
game1.shadow-land.de/www/public/assets/js/main.js

866 lines
29 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
})();