Files
Adminator 88e3c09e5c Per-Sim leistung_pct: alle 14 Sims senden das eine Leistungs-% (0-100)
Jede Sim liefert jetzt selbst leistung_pct in GGS_LIVE_STATE UND im Submit
(Fundament fuer das einheitliche %-System) — kein Rueckrechnen aus Sternen
mehr, keine 0-3-Sonderfaelle. Formeln je Sim wie vereinbart:
- klima/fluss/kofferdetektiv/heli: = vorhandener 0-100-score
- weltkueche: richtige/(richtige+falsche)*100 (Fehler in %)
- busfahrt/fluggesellschaft: topoPunkte/(Auftraege*3)*100
- farmer: gewichteter totalScore; energiemanager: ausgegl./gesamt -15/Blackout
- eu-werkstatt: firstTry/Schritte -5/Muelleimer; sonnensystem: Erstversuch-Quote
- tourismustal: Ziele+Zufriedenheit+Natur gewichtet; tourismusregion: gewichteter score
- logistik: Kontostand->% (sendet erstmals eine Kennzahl, auch an /api/assessment)
Teacher _leistungPct + live.php extractScore bevorzugen bereits diesen Wert.
Additive Aenderungen, bestehende Logik/Sterne unveraendert; alle 15 Dateien
syntaktisch geprueft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 12:35:56 +02:00

770 lines
37 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>✈ Fluggesellschaft — GeoGraSim</title>
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<style>
:root {
--fl-bg: #0a1d2e;
--fl-ink: #1f2937;
--fl-ink-2: #5b6b78;
--fl-good: #1b7e3b;
--fl-miss: #c0392b;
--fl-accent: #2563eb;
--fl-warm: #e8a247;
}
html, body {
margin: 0; padding: 0; height: 100dvh; overflow: hidden;
font-family: var(--ggs-font, system-ui, sans-serif);
background: var(--fl-bg);
color: var(--fl-ink);
}
#map { position: absolute; inset: 60px 0 0 0; z-index: 1; }
.leaflet-container { background: #0a1d2e; }
/* Flug-Status-Box: erscheint nur während Animation */
#flight-status {
position: absolute; top: 72px; left: 50%; transform: translateX(-50%);
z-index: 500; background: rgba(15, 28, 45, 0.92); color: #fff;
padding: 8px 16px; border-radius: 8px; font-size: 14px; font-weight: 600;
box-shadow: 0 4px 14px rgba(0,0,0,0.4); pointer-events: none;
border: 1px solid rgba(232, 162, 71, 0.5);
display: none; max-width: 90vw; text-align: center;
}
#flight-status.show { display: block; }
#flight-status .fs-progress { color: #e8a247; margin-left: 6px; }
.ggs-topbar {
position: relative; z-index: 100;
height: 60px; background: linear-gradient(135deg, #1f4b37, #2563eb);
color: #fff; display: flex; align-items: center; gap: 12px;
padding: 0 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.ggs-topbar h1 { font-size: 17px; margin: 0; font-weight: 700; }
.ggs-topbar a { color: #fff; text-decoration: none; padding: 4px 8px;
background: rgba(255,255,255,0.15); border-radius: 6px; font-size: 18px; }
.fl-budget, .fl-counter, .fl-night-pill, .fl-co2 {
font-size: 14px; padding: 4px 10px; background: rgba(255,255,255,0.15); border-radius: 6px;
}
.fl-night-pill { display: none; background: #ff9800; }
.fl-night-pill.show { display: inline-flex; }
.fl-toggle {
background: rgba(255,255,255,0.18); border: none; color: #fff; cursor: pointer;
padding: 4px 8px; border-radius: 6px; font-size: 16px; margin-left: auto;
}
/* Order-Card */
.fl-order-card {
position: absolute; top: 76px; right: 12px; z-index: 60;
width: 320px; max-width: calc(100vw - 24px);
background: rgba(255,255,255,0.95); border-radius: 10px;
box-shadow: 0 6px 18px rgba(0,0,0,0.25); padding: 14px;
}
.fl-order-card.hidden { display: none; }
.fl-order-card.resolved { background: #f0f7f2; }
.fl-order-card h3 { margin: 0 0 6px; font-size: 17px; color: #1f4b37; }
.fl-order-card p { margin: 4px 0; font-size: 14px; line-height: 1.4; }
.fl-attempts { display: flex; gap: 6px; margin: 8px 0; }
.fl-dot { width: 14px; height: 14px; border-radius: 50%; background: #e0e0e0; border: 2px solid #ccc; }
.fl-dot.good { background: #4caf50; border-color: #2e7d32; }
.fl-dot.ok { background: #ff9800; border-color: #ef6c00; }
.fl-dot.miss { background: #f44336; border-color: #b71c1c; }
.fl-phase-label { font-size: 12px; color: var(--fl-ink-2); margin-top: 4px; }
.fl-night-hint { font-size: 12px; color: #ef6c00; margin-top: 6px; }
.fl-order-hint { font-size: 13px; color: var(--fl-warm); margin-top: 6px; font-weight: 600; }
/* Info-Card */
.fl-info {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
z-index: 200; max-width: 440px; width: calc(100vw - 24px); max-height: 86vh; overflow: auto;
background: #fff; border-radius: 14px; box-shadow: 0 12px 40px rgba(0,0,0,0.4);
padding: 18px;
}
.fl-info.hidden { display: none; }
.fl-info-close { position: absolute; top: 8px; right: 12px;
background: transparent; border: none; font-size: 22px; cursor: pointer; color: #888; }
.fl-info img { width: 100%; max-height: 160px; object-fit: cover; border-radius: 8px; margin: 8px 0; }
.fl-info-meta { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0; }
.fl-info-meta .pill { background: #f0f0f0; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
.fl-margin {
margin: 10px 0; padding: 10px 12px; border-radius: 10px;
background: #f7f5ee; border: 1px solid #e0dccf;
}
.fl-margin.won { background: #e8f5e9; border-color: #a5d6a7; }
.fl-margin.lost { background: #ffebee; border-color: #ef9a9a; }
.fl-margin__headline { display: flex; justify-content: space-between; font-weight: 700; font-size: 17px; }
.fl-margin__lesson { font-size: 13px; color: #5b6b78; margin: 6px 0; }
.bf-calc { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; font-family: monospace; font-size: 13px; }
.bf-calc .v { text-align: right; }
.bf-calc .pos { color: #1b7e3b; }
.bf-calc .neg { color: #c0392b; }
.bf-calc .total { font-weight: 700; }
.bf-calc__sep { grid-column: 1 / -1; border-bottom: 1px dashed #ccc; }
.fl-info-next {
margin-top: 10px; padding: 10px 14px; background: #2563eb; color: #fff;
border: none; border-radius: 8px; font-size: 15px; cursor: pointer; width: 100%;
}
.fl-info-text { font-size: 13px; line-height: 1.5; color: #555; margin-top: 8px; }
.fl-tz-info { font-size: 12px; color: #2563eb; background: #e3f2fd; padding: 6px 10px; border-radius: 6px; margin: 6px 0; }
.fl-co2-info { font-size: 12px; color: #5b6b78; background: #fff8e1; padding: 6px 10px; border-radius: 6px; margin: 6px 0; }
/* Choice-Modal (Quiz) */
.ggs-modal-overlay { display: none; position: fixed; inset: 0; z-index: 300;
background: rgba(10, 29, 46, 0.75); align-items: center; justify-content: center; }
.ggs-modal-overlay.show { display: flex; }
.ggs-modal { background: #fff; max-width: 480px; width: calc(100vw - 24px);
padding: 20px; border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,0.4); max-height: 90vh; overflow: auto; }
.ggs-modal h2 { margin: 0 0 8px; color: #1f4b37; }
.ggs-modal p { font-size: 15px; line-height: 1.5; margin: 6px 0; }
.ggs-modal-actions { display: flex; gap: 8px; margin-top: 14px; justify-content: flex-end; }
.ggs-modal-actions button {
padding: 10px 16px; background: #2563eb; color: #fff;
border: none; border-radius: 8px; font-size: 14px; cursor: pointer;
}
.ggs-modal-actions button.secondary { background: #5b6b78; }
.fl-choices { display: grid; gap: 10px; margin: 12px 0; }
.fl-choice {
background: #f1ede2; border: 2px solid #d4cfc0; border-radius: 10px;
padding: 14px 16px; font-size: 16px; font-weight: 600; cursor: pointer; text-align: left;
min-height: 52px; color: var(--fl-ink); transition: background 0.1s;
}
.fl-choice.right { background: #c8e6c9; border-color: var(--fl-good); }
.fl-choice.wrong { background: #ffcdd2; border-color: var(--fl-miss); }
.fl-choice[disabled] { opacity: 0.6; cursor: default; }
.fl-choice-feedback { font-size: 14px; line-height: 1.4; margin-top: 8px; padding: 8px 10px; border-radius: 6px; }
.fl-choice-feedback.right { background: #e8f5e9; color: #1b5e20; }
.fl-choice-feedback.wrong { background: #ffebee; color: #b71c1c; }
/* Endscreen */
.fl-stars { font-size: 36px; text-align: center; margin: 8px 0; }
.fl-end-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 14px; }
.fl-end-row.final { border-top: 1px solid #ccc; margin-top: 6px; padding-top: 8px; font-weight: 700; font-size: 16px; }
.fl-verdict { margin-top: 10px; padding: 10px; background: #f0f7f2; border-radius: 8px; font-size: 14px; line-height: 1.5; }
.fl-logbook { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; }
.fl-logbook th, .fl-logbook td { padding: 4px 6px; border-bottom: 1px solid #eee; text-align: left; }
.fl-logbook .score-cell { text-align: center; }
.fl-logbook .margin-cell.pos { color: #1b7e3b; }
.fl-logbook .margin-cell.neg { color: #c0392b; }
.fl-logbook .night-row { background: #f5f5f5; }
.fl-logbook .lost-row td { color: #b71c1c; }
</style>
</head>
<body>
<div class="ggs-topbar">
<h1>✈ Fluggesellschaft</h1>
<a href="../../schueler.html" title="Zurück zur Schülerseite">🏠</a>
<span class="fl-budget" id="topbar-budget">💰 8.000 €</span>
<span class="fl-counter" id="topbar-counter"></span>
<span class="fl-night-pill" id="topbar-night">🌙 Nachtflug</span>
<span class="fl-co2" id="topbar-co2">🌍 0 t CO₂</span>
<button class="fl-toggle" id="toggle-map" onclick="toggleMapStyle()" title="Karte umschalten">🛰️</button>
</div>
<div id="map"></div>
<div id="flight-status"><span class="fs-text"></span><span class="fs-progress">0%</span></div>
<div class="fl-order-card hidden" id="order-card">
<h3 id="order-title"></h3>
<p id="order-prompt"></p>
<div class="fl-attempts">
<div class="fl-dot" id="dot-1"></div><div class="fl-dot" id="dot-2"></div><div class="fl-dot" id="dot-3"></div>
<span id="attempt-label" style="font-size:12px;color:#5b6b78">3 Versuche</span>
</div>
<div class="fl-phase-label" id="phase-label">Tippe den Flughafen auf der Karte</div>
<div class="fl-night-hint hidden" id="night-hint">🌙 Nachtflug — Karte ist abgedunkelt.</div>
<p class="fl-order-hint hidden" id="order-hint"></p>
</div>
<div class="fl-info hidden" id="info-card">
<button class="fl-info-close" onclick="closeInfo()"></button>
<h3 id="info-title"></h3>
<img id="info-img" alt="" onerror="this.style.display='none'">
<div class="fl-info-meta">
<span class="pill" id="info-country"></span>
<span class="pill" id="info-pop"></span>
<span class="pill" id="info-continent"></span>
<span class="pill" id="info-capital"></span>
</div>
<div class="fl-tz-info" id="info-tz"></div>
<div class="fl-co2-info" id="info-co2"></div>
<div class="fl-margin" id="info-margin">
<div class="fl-margin__headline">
<span id="margin-headline-label">Marge</span>
<span id="margin-headline-value">— €</span>
</div>
<div class="fl-margin__lesson" id="margin-lesson"></div>
<div id="margin-rows"></div>
</div>
<p class="fl-info-text" id="info-text"></p>
<button class="fl-info-next" onclick="nextOrder()" id="info-next-btn">Nächster Flug →</button>
</div>
<!-- Welcome -->
<div class="ggs-modal-overlay" id="welcome-overlay">
<div class="ggs-modal">
<h2>✈ Willkommen in der Flugleitung</h2>
<p>Du leitest eine Fluggesellschaft mit Hub am Frankfurter Flughafen (FRA).
Aufträge kommen rein — du musst Maschinen zu den richtigen Welt-Zielen schicken.</p>
<p><strong>So geht es:</strong></p>
<ul>
<li>Lies den Auftrag, beantworte die Zwischenfrage (Land / Kontinent).</li>
<li>Tippe den Ziel-Flughafen auf der Karte. 3 Versuche.</li>
<li>Daneben getippt = Umweg = Treibstoff = weniger Marge.</li>
<li>Treibstoff produziert CO₂ — wir zeigen die Bilanz mit.</li>
<li>Ziel: <strong id="welcome-target">— €</strong> am Tour-Ende.</li>
</ul>
<div class="ggs-modal-actions">
<button onclick="startGame()">Los arbeiten</button>
</div>
</div>
</div>
<!-- Quiz-Modal -->
<div class="ggs-modal-overlay" id="choice-overlay">
<div class="ggs-modal">
<h2 id="choice-title">Frage</h2>
<p id="choice-prompt"></p>
<div class="fl-choices" id="choices-container"></div>
<div class="fl-choice-feedback" id="choice-feedback" style="display:none"></div>
<div class="ggs-modal-actions">
<button id="choice-continue" onclick="afterChoice()" style="display:none">Flughafen wählen</button>
</div>
</div>
</div>
<!-- Endscreen -->
<div class="ggs-modal-overlay" id="end-overlay">
<div class="ggs-modal">
<h2>✈ Tour beendet</h2>
<div class="fl-stars" id="end-stars">⭐⭐⭐</div>
<div>
<div class="fl-end-row"><span>Topografie-Punkte:</span><span id="end-topo-points"></span></div>
<div class="fl-end-row"><span>Aufträge gewonnen:</span><span id="end-won-count"></span></div>
<div class="fl-end-row"><span>Quiz richtig auf 1. Versuch:</span><span id="end-quiz-firsttry"></span></div>
<div class="fl-end-row"><span>Startbudget:</span><span>8.000 €</span></div>
<div class="fl-end-row"><span>Verdienst aus Flügen:</span><span id="end-earnings"></span></div>
<div class="fl-end-row" id="end-quiz-row" style="display:none"><span>Quiz-Bonus / -Strafe:</span><span id="end-quiz-delta"></span></div>
<div class="fl-end-row final"><span>Endbudget:</span><span id="end-budget">— €</span></div>
<div class="fl-end-row"><span>🌍 Gesamt-CO₂ Tour:</span><span id="end-co2"></span></div>
<div class="fl-verdict" id="end-verdict"></div>
</div>
<table class="fl-logbook" id="end-logbook"></table>
<div class="ggs-modal-actions">
<button class="secondary" onclick="location.reload()">Nochmal arbeiten</button>
<button onclick="window.location.href='../../schueler.html'">Zur Übersicht</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="engine.js"></script>
<script>
// ===================================================================
// Fluggesellschaft — UI
// ===================================================================
const FORCED_LEVEL = (window.FLUG_FORCED_LEVEL || 'l1');
const STUDENT_EASY = !!window.STUDENT_EASY;
function pickText(t) {
if (!t) return '';
if (typeof t === 'string') return t;
return STUDENT_EASY && t.easy ? t.easy : (t.de || t.easy || '');
}
function formatEur(n) { return (Math.abs(n) >= 1000 ? n.toLocaleString('de-AT') : String(Math.round(n))) + ' €'; }
function formatKm(n) { return Math.round(n).toLocaleString('de-AT') + ' km'; }
function formatPop(m) { return (m >= 1 ? m.toFixed(1) + ' Mio.' : (m * 1000).toFixed(0) + ' Tsd.') + ' Einw.'; }
function formatCO2t(kg) { return (kg / 1000).toFixed(1) + ' t'; }
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
// Leaflet-Karte
const tilesEsri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri', maxZoom: 18
});
const tilesOsm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap', maxZoom: 18
});
const tilesDark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', {
attribution: '&copy; CARTO &copy; OpenStreetMap', maxZoom: 18
});
let currentTileMode = 'esri'; // 'esri' | 'osm' | 'dark'
const map = L.map('map', { worldCopyJump: true, minZoom: 3, maxZoom: 8 }).setView([45, 15], 4);
L.control.scale({ metric: true, imperial: false, position: 'bottomleft', maxWidth: 200 }).addTo(map);
tilesEsri.addTo(map);
function toggleMapStyle() {
if (currentTileMode === 'esri') { map.removeLayer(tilesEsri); tilesOsm.addTo(map); currentTileMode = 'osm'; }
else if (currentTileMode === 'osm') { map.removeLayer(tilesOsm); tilesDark.addTo(map); currentTileMode = 'dark'; }
else { map.removeLayer(tilesDark); tilesEsri.addTo(map); currentTileMode = 'esri'; }
}
// Flughafen-Icon (✈) + Bus/Flug-Marker
const planeIcon = L.divIcon({ html: '<div style="font-size:24px;text-shadow:0 1px 3px #000">✈</div>',
iconSize: [24, 24], iconAnchor: [12, 12], className: '' });
const targetIcon = L.divIcon({ html: '<div style="font-size:28px;text-shadow:0 1px 3px #000">📍</div>',
iconSize: [28, 28], iconAnchor: [14, 28], className: '' });
const tapIcon = L.divIcon({ html: '<div style="font-size:22px">❓</div>',
iconSize: [22, 22], iconAnchor: [11, 11], className: '' });
let planeMarker = null, currentTapMarker = null, currentTargetMarker = null, currentLine = null;
let mapInteractive = false;
let game = null;
let totalOrders = 0;
(async function init() {
await window.FlugEngine.loadData();
const lvl = FORCED_LEVEL;
const seed = (window.FLUG_SESSION_ID ? hashStr(String(window.FLUG_SESSION_ID)) : 0) || Date.now();
game = window.FlugEngine.init(lvl, seed);
totalOrders = game.tourOrders.length;
const home = window.FlugEngine.getCity(game.lastCity);
if (home) {
planeMarker = L.marker([home.lat, home.lon], { icon: planeIcon }).addTo(map);
map.flyTo([home.lat, home.lon], 4, { duration: 0.6 });
}
updateTopbar();
const ECON = window.FlugEngine.ECON;
const target = (typeof ECON.targetBudget === 'object')
? (ECON.targetBudget[lvl] || 14000)
: ECON.targetBudget;
document.getElementById('welcome-target').textContent = formatEur(target);
document.getElementById('welcome-overlay').classList.add('show');
})();
function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h<<5)-h + s.charCodeAt(i))|0; return h>>>0; }
function haversineKmLocal(lat1, lon1, lat2, lon2) {
const R = 6371, toRad = d => d * Math.PI / 180;
const dLat = toRad(lat2 - lat1), dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat/2)**2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon/2)**2;
return 2 * R * Math.asin(Math.sqrt(a));
}
function updateTopbar() {
if (!game) return;
document.getElementById('topbar-budget').textContent = '💰 ' + formatEur(game.budget);
const idx = Math.min(game.orderIndex + 1, totalOrders);
document.getElementById('topbar-counter').textContent = `Flug ${idx} / ${totalOrders}`;
document.getElementById('topbar-co2').textContent = '🌍 ' + formatCO2t(game.co2Kg) + ' CO₂';
const order = game.currentOrder || (game.tourOrders[game.orderIndex] || null);
document.getElementById('topbar-night').classList.toggle('show', !!(order && order.isNight));
}
function startGame() {
document.getElementById('welcome-overlay').classList.remove('show');
if (game && !game._startedAt) game._startedAt = Date.now();
loadNextOrder();
}
function loadNextOrder() {
clearMapMarkers();
document.getElementById('order-hint').classList.add('hidden');
document.getElementById('info-card').classList.add('hidden');
resetAttemptDots();
const order = window.FlugEngine.nextOrder(game);
if (!order) { showEndScreen(); return; }
if (order.isNight) { /* Karte abdunkeln per Mode-Switch */ if (currentTileMode !== 'dark') { /* nicht erzwingen */ } }
renderOrderCard(order);
updateTopbar();
if (game.currentQuestion) { showChoiceModal(order); mapInteractive = false; }
else { document.getElementById('order-card').classList.remove('hidden'); mapInteractive = true; }
}
function renderOrderCard(order) {
const card = document.getElementById('order-card');
card.classList.remove('hidden', 'resolved');
const target = window.FlugEngine.getCity(order.cityId);
document.getElementById('order-title').textContent = target ? target.title : 'Ziel';
document.getElementById('order-prompt').textContent = pickText(order.prompt);
document.getElementById('attempt-label').textContent = '3 Versuche';
document.getElementById('phase-label').textContent = game.currentQuestion ? 'Erst Quiz beantworten' : 'Tippe den Flughafen';
document.getElementById('night-hint').classList.toggle('hidden', !order.isNight);
}
function resetAttemptDots() {
for (let i = 1; i <= 3; i++) document.getElementById('dot-' + i).classList.remove('miss', 'ok', 'good');
}
function showChoiceModal(order) {
const q = game.currentQuestion; if (!q) return;
const ov = document.getElementById('choice-overlay');
document.getElementById('choice-title').textContent = q.kind === 'capital' ? 'Hauptstadt-Frage' : (q.kind === 'continent' ? 'Kontinent-Frage' : 'Länder-Frage');
document.getElementById('choice-prompt').textContent = pickText(q.prompt);
const cont = document.getElementById('choices-container'); cont.innerHTML = '';
const fb = document.getElementById('choice-feedback'); fb.style.display = 'none';
document.getElementById('choice-continue').style.display = 'none';
q.choiceLabels.forEach((label, idx) => {
const btn = document.createElement('button');
btn.className = 'fl-choice'; btn.textContent = label;
btn.onclick = () => onChoicePick(btn, idx, cont);
cont.appendChild(btn);
});
ov.classList.add('show');
}
function onChoicePick(btn, idx, container) {
const result = window.FlugEngine.evaluateQuestion(game, idx);
if (!result) return;
updateTopbar();
const fb = document.getElementById('choice-feedback');
if (result.correct) {
btn.classList.add('right');
Array.from(container.children).forEach(b => b.disabled = true);
const q = game.currentQuestion;
const correctLabel = q.choiceLabels[q.correctIndex];
let msg = result.bonusEur > 0
? `Richtig! +${result.bonusEur} € Bonus für die erste richtige Antwort.`
: `Richtig: ${correctLabel}. ${result.penaltyEur > 0 ? `(${result.attempts.length-1} Fehlversuche haben ${result.penaltyEur} € gekostet.)` : ''}`;
fb.textContent = msg; fb.className = 'fl-choice-feedback right'; fb.style.display = '';
document.getElementById('choice-continue').style.display = 'inline-flex';
} else {
btn.classList.add('wrong'); btn.disabled = true;
const penaltyTbl = window.FlugEngine.ECON.quizPenaltyPerWrongEur;
const penalty = (typeof penaltyTbl === 'object') ? (penaltyTbl[game.levelId] || penaltyTbl.l1 || 100) : penaltyTbl;
fb.textContent = `Nicht ganz. ${penalty} € Strafe. Versuch noch eine Antwort.`;
fb.className = 'fl-choice-feedback wrong'; fb.style.display = '';
}
}
function afterChoice() {
document.getElementById('choice-overlay').classList.remove('show');
mapInteractive = true;
document.getElementById('phase-label').textContent = 'Tippe den Flughafen';
}
map.on('click', async (e) => {
if (!mapInteractive || !game || !game.currentOrder) return;
const tap = { lat: e.latlng.lat, lon: e.latlng.lng };
const result = window.FlugEngine.checkTap(game, tap);
if (!result) return;
paintAttempt(tap);
updateAttemptDot(game.attempts.length, result.score);
if (result.resolved) {
mapInteractive = false;
document.getElementById('attempt-label').textContent =
result.score === 3 ? '✓ Treffer!' : (result.score === 1 ? 'Ungenau' : 'Versuche aufgebraucht');
document.getElementById('order-hint').classList.add('hidden');
document.getElementById('phase-label').textContent = '✈ Flug läuft …';
await runResolution(tap);
} else {
showHint(result);
document.getElementById('attempt-label').textContent = `Versuch ${game.attempts.length + 1} / 3`;
setTimeout(() => clearTapMarker(), 1500);
}
});
function paintAttempt(tap) {
if (currentTapMarker) map.removeLayer(currentTapMarker);
currentTapMarker = L.marker([tap.lat, tap.lon], { icon: tapIcon }).addTo(map);
}
function updateAttemptDot(n, score) {
const d = document.getElementById('dot-' + n); if (!d) return;
if (score === 3) d.classList.add('good');
else if (score === 1) d.classList.add('ok');
else d.classList.add('miss');
}
function showHint(result) {
const h = document.getElementById('order-hint');
h.textContent = result.hint; h.classList.remove('hidden');
}
function clearTapMarker() { if (currentTapMarker) { map.removeLayer(currentTapMarker); currentTapMarker = null; } }
function clearMapMarkers() {
clearTapMarker();
if (currentTargetMarker) { map.removeLayer(currentTargetMarker); currentTargetMarker = null; }
if (currentLine) { map.removeLayer(currentLine); currentLine = null; }
}
async function runResolution(tap) {
const order = game.currentOrder;
const target = window.FlugEngine.getCity(order.cityId);
const fromCity = window.FlugEngine.getCity(game.lastCity);
if (!target || !fromCity) return;
// Großkreis-Polyline zeichnen (Plot der echten Flugbahn)
const pts = window.FlugEngine.greatCirclePoints(fromCity.lat, fromCity.lon, target.lat, target.lon, 64);
currentLine = L.polyline(pts, { color: '#e8a247', weight: 3, opacity: 0.9, dashArray: '6 4' }).addTo(map);
if (currentTargetMarker) map.removeLayer(currentTargetMarker);
currentTargetMarker = L.marker([target.lat, target.lon], { icon: targetIcon }).addTo(map);
map.flyToBounds(L.latLngBounds(pts).pad(0.2), { duration: 0.9, maxZoom: 4 });
// Flugzeug-Animation entlang der Linie — distanzbasiert.
// Bearbeiter:in soll die Route verfolgen können: kurze Flüge ~3s, lange ~12s.
const start = pts[0]; const end = pts[pts.length - 1];
const distanceKm = haversineKmLocal(start[0], start[1], end[0], end[1]);
const totalDurationMs = Math.max(3000, Math.min(12000, distanceKm * 1.5));
const stepMs = totalDurationMs / pts.length;
// Status-Box oberhalb der Karte einblenden
const statusEl = document.getElementById('flight-status');
const statusText = statusEl ? statusEl.querySelector('.fs-text') : null;
const statusProg = statusEl ? statusEl.querySelector('.fs-progress') : null;
if (statusEl) {
if (statusText) statusText.textContent = `${fromCity.title}${target.title} · ${Math.round(distanceKm).toLocaleString('de-AT')} km · Fortschritt`;
if (statusProg) statusProg.textContent = '0%';
statusEl.classList.add('show');
}
if (planeMarker) {
let i = 0;
await new Promise(res => {
const step = () => {
i++;
if (i >= pts.length) {
planeMarker.setLatLng(end);
if (statusProg) statusProg.textContent = '100%';
res();
return;
}
planeMarker.setLatLng(pts[i]);
if (statusProg) statusProg.textContent = Math.round((i / pts.length) * 100) + '%';
setTimeout(step, stepMs);
};
step();
});
}
if (statusEl) setTimeout(() => statusEl.classList.remove('show'), 700);
const report = window.FlugEngine.settleOrder(game);
updateTopbar();
showInfoCard(report);
}
function showInfoCard(report) {
const city = window.FlugEngine.getCity(report.cityId);
if (!city) return;
document.getElementById('info-title').textContent = city.title;
const img = document.getElementById('info-img');
if (city.imagePath) { img.src = (window.FLUG_BASE || '') + city.imagePath; img.style.display = ''; }
else img.style.display = 'none';
document.getElementById('info-country').textContent = '🌍 ' + city.country;
document.getElementById('info-pop').textContent = '👥 ' + formatPop(city.population_million || 0);
document.getElementById('info-continent').textContent = '🗺 ' + (city.continent || '');
const cap = document.getElementById('info-capital');
if (city.isCapital) { cap.textContent = '⭐ Hauptstadt'; cap.style.display = ''; } else { cap.style.display = 'none'; }
// Zeitzone-Info
const tz = document.getElementById('info-tz');
const tzDiff = report.tzDiffHours || 0;
if (tzDiff === 0) tz.textContent = `🕐 Selbe Zeitzone wie Startflughafen.`;
else if (tzDiff > 0) tz.textContent = `🕐 ${city.title} ist ${tzDiff}h VORAUS — wenn es bei dir 12:00 ist, dort ${(12+tzDiff)%24}:00.`;
else tz.textContent = `🕐 ${city.title} ist ${Math.abs(tzDiff)}h ZURÜCK — wenn es bei dir 12:00 ist, dort ${(12+tzDiff+24)%24}:00.`;
// CO2-Info
document.getElementById('info-co2').textContent =
`🌳 Dieser Flug verursachte ${report.co2Kg.toLocaleString('de-AT')} kg CO₂ (gerechnet auf ${window.FlugEngine.ECON.paxOnboard} Passagiere). Ein Hektar Wald bindet ca. 10 t CO₂/Jahr.`;
// Marge-Block analog Bus
const detourKm = Math.round(report.detourKm);
const quizBonus = report.quizBonus || 0;
const quizPenalty = report.quizPenalty || 0;
const quizDelta = quizBonus - quizPenalty;
const tourMargin = report.margin + quizDelta;
const tourMargClass = tourMargin >= 0 ? 'pos' : 'neg';
document.getElementById('margin-headline-value').textContent =
(tourMargin >= 0 ? '+' : '') + formatEur(Math.abs(tourMargin));
document.getElementById('margin-headline-label').textContent = tourMargin >= 0 ? 'Marge' : 'Verlust';
document.getElementById('info-margin').className = 'fl-margin ' + (tourMargin >= 0 ? 'won' : 'lost');
let lesson;
if (detourKm <= 50) lesson = '✓ Punktlandung am Flughafen — volle Marge.';
else if (detourKm < 500) lesson = `Kleiner Umweg von ${detourKm} km — kostet ${formatEur(detourKm * 0.13 * 180)}, frisst aber noch nicht alles.`;
else lesson = `Großer Umweg von ${detourKm} km — Treibstoff frisst die Marge weg.`;
document.getElementById('margin-lesson').textContent = lesson;
const rows = document.getElementById('margin-rows');
const fmtSigned = n => (n >= 0 ? '+' : '') + formatEur(Math.abs(n));
const subtotal = report.baseMargin;
const subClass = subtotal >= 0 ? 'pos' : 'neg';
let html = '<div class="bf-calc">';
html += `<div>Direkte Flugstrecke</div><div class="v">${formatKm(report.realDistanceKm)}</div>`;
html += `<div>Tatsächlich geflogen</div><div class="v">${formatKm(report.actualDistanceKm)}</div>`;
if (detourKm > 0) html += `<div>davon Umweg</div><div class="v">+${formatKm(detourKm)}</div>`;
html += '<div class="bf-calc__sep"></div>';
html += `<div>Einnahmen (180 Pax × Tarif)</div><div class="v pos">+${formatEur(report.playerPrice)}</div>`;
html += `<div>Ausgaben (Treibstoff + Flughafengeb.)</div><div class="v neg">${formatEur(report.busCost)}</div>`;
html += '<div class="bf-calc__sep"></div>';
if (quizBonus > 0 || quizPenalty > 0) {
html += `<div class="total">Zwischensumme</div><div class="v total ${subClass}">${fmtSigned(subtotal)}</div>`;
if (quizBonus > 0) html += `<div>Quiz-Bonus (1. Versuch)</div><div class="v pos">+${formatEur(quizBonus)}</div>`;
if (quizPenalty > 0) html += `<div>Quiz-Strafe (Fehlversuche)</div><div class="v neg">${formatEur(quizPenalty)}</div>`;
html += '<div class="bf-calc__sep"></div>';
}
html += `<div class="total">Marge</div><div class="v total ${tourMargClass}">${fmtSigned(tourMargin)}</div>`;
html += '</div>';
rows.innerHTML = html;
document.getElementById('info-text').textContent = pickText(city.infoText);
const isLast = game.orderIndex >= totalOrders;
document.getElementById('info-next-btn').textContent = isLast ? 'Auswertung →' : 'Nächster Flug →';
document.getElementById('info-card').classList.remove('hidden');
}
function closeInfo() {
document.getElementById('info-card').classList.add('hidden');
loadNextOrder();
}
function nextOrder() { closeInfo(); }
function showEndScreen() {
const ECON = window.FlugEngine.ECON;
const maxTopo = totalOrders * 3;
const ratio = game.score / maxTopo;
let stars;
if (ratio >= 0.85) stars = '⭐⭐⭐';
else if (ratio >= 0.55) stars = '⭐⭐';
else if (ratio >= 0.30) stars = '⭐';
else stars = '—';
document.getElementById('end-stars').textContent = stars;
const wonCount = game.reports.filter(r => r.won).length;
const quizFirstTry = game.reports.filter(r => r.quizFirstTry).length;
const quizBonusSum = game.reports.reduce((s, r) => s + (r.quizBonus || 0), 0);
const quizPenaltySum = game.reports.reduce((s, r) => s + (r.quizPenalty || 0), 0);
const quizDelta = quizBonusSum - quizPenaltySum;
document.getElementById('end-topo-points').textContent = `${game.score} / ${maxTopo}`;
document.getElementById('end-won-count').textContent = `${wonCount} / ${totalOrders}`;
document.getElementById('end-quiz-firsttry').textContent = `${quizFirstTry} / ${totalOrders}`;
document.getElementById('end-earnings').textContent = (game.earnings >= 0 ? '+' : '') + formatEur(game.earnings);
if (quizBonusSum > 0 || quizPenaltySum > 0) {
document.getElementById('end-quiz-row').style.display = '';
document.getElementById('end-quiz-delta').textContent = (quizDelta >= 0 ? '+' : '') + formatEur(Math.abs(quizDelta));
}
document.getElementById('end-budget').textContent = formatEur(game.budget);
document.getElementById('end-co2').textContent = formatCO2t(game.co2Kg) + ' (= ' + Math.round(game.co2Kg / 10000) + ' Hektar Wald, 1 Jahr Bindung)';
const target = (typeof ECON.targetBudget === 'object') ? (ECON.targetBudget[game.levelId] || 14000) : ECON.targetBudget;
const verdEl = document.getElementById('end-verdict');
if (game.budget >= target) verdEl.innerHTML = `🎉 Super! Tour-Ziel von <strong>${formatEur(target)}</strong> erreicht.`;
else if (game.budget >= ECON.startBudget) verdEl.innerHTML = `Solide. Ziel war ${formatEur(target)}.`;
else verdEl.innerHTML = `🚧 Verlust. Tipp: Flughäfen genauer treffen, Quiz auf 1. Versuch lösen.`;
const tbl = document.getElementById('end-logbook');
tbl.innerHTML = '<tr><th>#</th><th>Flug</th><th>Topo</th><th>Marge</th></tr>';
game.reports.forEach((r, i) => {
const fromCity = window.FlugEngine.getCity(r.fromCityId);
const toCity = window.FlugEngine.getCity(r.cityId);
const fromName = fromCity ? fromCity.title : r.fromCityId;
const toName = toCity ? toCity.title : r.cityId;
const scoreEmoji = r.finalScore === 3 ? '🟢' : (r.finalScore === 1 ? '🟠' : '🔴');
const quizDeltaRow = (r.quizBonus || 0) - (r.quizPenalty || 0);
const tourMargin = r.margin + quizDeltaRow;
const margClass = tourMargin > 0 ? 'pos' : (tourMargin < 0 ? 'neg' : '');
const margTxt = (tourMargin >= 0 ? '+' : '') + formatEur(tourMargin);
let quizBadge = r.quizFirstTry ? ' · 🧠 ✓' : (r.quizAttemptsCount > 1 ? ` · 🧠 ${r.quizAttemptsCount}×` : '');
const tr = document.createElement('tr');
if (tourMargin < 0) tr.className = 'lost-row';
if (r.isNight) tr.classList.add('night-row');
tr.innerHTML =
`<td>${i + 1}</td>` +
`<td>${fromName} → <strong>${toName}</strong><br><small>${Math.round(r.realDistanceKm)} km · ${r.co2Kg.toLocaleString('de-AT')} kg CO₂${quizBadge}</small></td>` +
`<td class="score-cell">${scoreEmoji}</td>` +
`<td class="margin-cell ${margClass}">${margTxt}</td>`;
tbl.appendChild(tr);
});
document.getElementById('order-card').classList.add('hidden');
document.getElementById('info-card').classList.add('hidden');
clearMapMarkers();
document.getElementById('end-overlay').classList.add('show');
// Submit an die Plattform
submitTourToBackend();
}
// ===== Submit-Hook (Lehrer-Cockpit, Klassen-Vergleich) =====
function submitTourToBackend() {
if (!window.FLUG_SESSION_ID) return; // Free-Mode: nicht senden
const api = (window.FLUG_API_BASE || '/geograsim/App/php/api') + '/progress.php';
const total = game.tourOrders.length || 1;
const wonCount = game.reports.filter(r => r.won).length;
const quizFirstTry = game.reports.filter(r => r.quizFirstTry).length;
const stars = (() => {
const ratio = game.score / (total * 3);
return ratio >= 0.85 ? 3 : (ratio >= 0.55 ? 2 : (ratio >= 0.30 ? 1 : 0));
})();
try {
fetch(api, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
sim_id: 'fluggesellschaft',
action: 'submit_assessment',
data: {
level: game.levelId, stars,
score: Math.round(game.score),
duration_ms: Date.now() - (game._startedAt || Date.now()),
completed: true,
results: {
budget: game.budget,
earnings: game.earnings,
co2Kg: game.co2Kg,
wonOrders: wonCount,
totalOrders: total,
quizFirstTry,
quizFailCount: game.reports.reduce((s,r)=>s+Math.max(0,(r.quizAttemptsCount||1)-1), 0),
leistung_pct: (total*3)===0 ? null : Math.max(0,Math.min(100,Math.round(game.score/(total*3)*100))),
}
}
})
}).catch(()=>{});
} catch (_) {}
}
// ===== LIVE-State für Lehrer*innen-Cockpit =====
window.GGS_LIVE_STATE = function() {
if (!game) return null;
const reports = game.reports || [];
const wonCount = reports.filter(r => r.won).length;
const lastR = reports[reports.length - 1] || null;
const lastCity = window.FlugEngine.getCity(game.lastCity);
return {
simId: 'fluggesellschaft',
levelId: game.levelId,
orderIndex: game.orderIndex,
totalOrders: game.tourOrders.length,
budget: game.budget,
earnings: game.earnings,
co2Kg: game.co2Kg,
wonOrders: wonCount,
lostOrders: reports.length - wonCount,
lastCityId: game.lastCity,
lastCityTitle: lastCity ? lastCity.title : null,
lastMargin: lastR ? lastR.margin : null,
lastTzDiff: lastR ? lastR.tzDiffHours : null,
quizFirstTry: reports.filter(r => r.quizFirstTry).length,
leistung_pct: (game.tourOrders.length*3)===0 ? null : Math.max(0,Math.min(100,Math.round(game.score/(game.tourOrders.length*3)*100))),
timestamp: Date.now(),
};
};
</script>
<script src="../../assets/js/ggs-quick-intro.js" defer></script>
<script>
window.addEventListener('DOMContentLoaded', function () {
if (window.GgsQuickIntro) GgsQuickIntro.init({
simId: 'fluggesellschaft',
title: '✈️ Fluggesellschaft',
lead: 'Du fliegst Aufträge rund um den Globus. Hauptkostenpunkt: CO₂ und Zeitzonen. Wo lohnt sich die weite Reise, wo nicht?',
steps: [
'Auftrag wählen — du siehst Start, Ziel und Bezahlung.',
'Auf der Weltkarte das richtige Land/die richtige Stadt anklicken.',
'Quiz beantworten — Land und Kontinent richtig zuordnen.',
'CO₂-Bilanz im Blick — pro Flug + pro Sitzung.',
],
goals: [
'Globale Geografie sicher zuordnen',
'Zeitzonen verstehen und nutzen',
'CO₂-Wirkung von Fernreisen einschätzen',
],
});
});
</script>
</body>
</html>