From 0fe4377debc29217216eb77b87fdac20ec57a8ef Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 19 Apr 2026 14:08:04 +0200 Subject: [PATCH] =?UTF-8?q?Klima=203D:=20Heli=20pro=20Airport,=20Flugzeug,?= =?UTF-8?q?=20Flut-T=C3=B6nung,=20Graph-Top-Verlust?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Airport-Fahrzeuge: pro Flughafen eigener Heli + Flugzeug, in airportVehicles-Map verwaltet, über syncMeasureMeshes mit-synchronisiert. Phasen-Offsets per Airport-Index, damit mehrere Flughäfen nicht synchron laufen. - Heli: landet jetzt korrekt auf dem H (padLocal aus userData statt hardcoded, inkl. LIFT-Korrektur). Alle ~3 Zyklen (deterministisch) Vulkan-Runde statt Airport-Kreis — Radius 9 um VOLCANO_X/Z. - Flugzeug (makePlane, low-poly Cessna): 110-s-Zyklus — parkt am Runway-Anfang, Takeoff-Roll, Climb-Out nach -X, 50 s unsichtbar (Reise), Approach, Landing-Roll, Taxi. Propeller dreht. - Wolken: Höhe auf ~7 abgesenkt (Vulkan-Gletscher-Höhe), fühlen sich jetzt wie Dunst um den Kegel an statt Himmels-Schleier. - Flut-Tönung: applyFloodTintAll lerpt Material-Farben aller Mesh- Füße, die unter ocean.position.y liegen, Richtung Dunkelgrau und kippt sie bis 0.12 rad. Wirkt auf Maßnahmen + Dorf + Citizen-Strand. - Graph-Top = verloren: neu endLevel(false, 'co2-top' | 'temp-top') wenn state.co2Ppm ≥ 700 oder state.currentTemp ≥ 19. Endscreen bekommt passende Icons (🌫 / 🌡) und Titel. Co-Authored-By: Claude Opus 4.7 (1M context) --- App/sims/klima/game-3d.html | 367 ++++++++++++++++++++++++++++++------ 1 file changed, 305 insertions(+), 62 deletions(-) diff --git a/App/sims/klima/game-3d.html b/App/sims/klima/game-3d.html index c305796..c3d7407 100644 --- a/App/sims/klima/game-3d.html +++ b/App/sims/klima/game-3d.html @@ -1646,7 +1646,17 @@ function simulateTick() { reportLiveProgress(); } - if (res.endReason === 'won') endLevel(true); + // Graph-Top = verloren: sobald CO₂ oder Temperatur die Obergrenze der + // Diagramme (700 ppm / 19 °C) erreicht, ist der Durchgang gekippt. + // Vor der Engine-endReason-Kette geprüft, damit Grenzwert-Verluste + // auch am MaxTick (würde sonst als 'won' zählen) noch greifen. + if (state.co2Ppm >= 700) { + state.levelWon = false; state.levelCompleted = true; + endLevel(false, 'co2-top'); + } else if (state.currentTemp >= 19) { + state.levelWon = false; state.levelCompleted = true; + endLevel(false, 'temp-top'); + } else if (res.endReason === 'won') endLevel(true); else if (res.endReason === 'pleite') endLevel(false, 'pleite'); else if (res.endReason === 'ueberflutet') endLevel(false, 'ueberflutet'); else { @@ -2569,6 +2579,11 @@ function sceneRender() { if (rotor) rotor.rotation.z = tNow * 1.4; } + // Flut-Tönung: sobald ein Gebäude unter Wasser gerät, Materialien + // Richtung dunkelgrau/schwarz lerpen + leicht nach hinten kippen + // ("defekt"-Look). Gilt für Maßnahmen, Dorf-Häuser und Citizen-Strand. + applyFloodTintAll(); + // Mangroven-Gesundheit: ab 30 cm Meeresspiegel fangen sie an zu sterben, // bei 70 cm sind sie komplett grau. Standard-Farben werden Richtung // grau/dunkelbraun interpoliert — Versalzung + Überflutung sichtbar. @@ -3095,6 +3110,10 @@ function syncMeasureMeshes() { measuresGroup.remove(mesh); placedMeshes.delete(key); } + // Airport-Fahrzeuge (Heli + Flugzeug) an geänderten Airport-Bestand + // anpassen — muss nach syncMeasureMeshes laufen, damit der Airport-Mesh + // schon in placedMeshes liegt. + if (typeof syncAirportVehicles === 'function') syncAirportVehicles(); } /* ============================================================ @@ -3857,81 +3876,244 @@ function makeHeli() { skid2.position.z = -0.15; g.add(skid2); return g; } -const heli = makeHeli(); -heli.visible = false; -heli.userData.infoKey = 'heli_info'; -ambientGroup.add(heli); +// --- Flugzeug (Cessna-artig, low-poly) --- +// Wird pro Flughafen einmal angelegt. Macht einen langen Zyklus (Parken, +// Start, Wegflug, Rückflug, Landung, Taxi). Propeller dreht konstant. +function makePlane() { + const g = new THREE.Group(); + const bodyMat = new THREE.MeshStandardMaterial({ color: 0xe8e8ea, roughness: 0.7 }); + const trimMat = new THREE.MeshStandardMaterial({ color: 0x2a4a6a, roughness: 0.8 }); + const darkMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 1 }); -// Heli-Flug-Zyklus (Sekunden): orbit → landing → sitzt → takeoff → orbit … -// Der Pad sitzt durch den 2x-Scale am Airport bei (inst.pos + padLocal*2). -const HELI_CYCLE = 36; -function updateHeli(tNow) { + // Rumpf (Cylinder entlang X, Nase +X) + const fuselage = new THREE.Mesh( + new THREE.CylinderGeometry(0.14, 0.10, 1.2, 10), + bodyMat + ); + fuselage.rotation.z = Math.PI / 2; + g.add(fuselage); + // Cockpit (leicht dunkler Block oben) + const cockpit = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.13, 0.22), trimMat); + cockpit.position.set(0.12, 0.13, 0); g.add(cockpit); + // Flügel (über dem Rumpf, Hochdecker) + const wing = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.04, 1.6), bodyMat); + wing.position.set(0.0, 0.20, 0); g.add(wing); + // Heckflosse (vertikal) + const tail = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.25, 0.04), bodyMat); + tail.position.set(-0.55, 0.25, 0); g.add(tail); + // Höhenleitwerk (horizontal, Heck) + const htail = new THREE.Mesh(new THREE.BoxGeometry(0.20, 0.03, 0.5), bodyMat); + htail.position.set(-0.55, 0.18, 0); g.add(htail); + // Propeller-Nabe + Blätter + const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.08, 8), darkMat); + hub.rotation.z = Math.PI / 2; + hub.position.set(0.62, 0, 0); g.add(hub); + const prop = new THREE.Group(); + prop.position.set(0.66, 0, 0); + const blade = new THREE.Mesh(new THREE.BoxGeometry(0.01, 0.55, 0.04), darkMat); + prop.add(blade); + g.add(prop); + g.userData.propRef = prop; + // Fahrwerk (zwei kleine Räder) + for (const sx of [-0.1, 0.1]) { + const strut = new THREE.Mesh(new THREE.CylinderGeometry(0.01, 0.01, 0.15, 6), darkMat); + strut.position.set(sx, -0.14, 0); g.add(strut); + const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.04, 10), darkMat); + wheel.rotation.x = Math.PI / 2; + wheel.position.set(sx, -0.22, 0); g.add(wheel); + } + return g; +} + +// --- Airport-Fahrzeuge pro Flughafen --- +// airportVehicles hält pro gebautem Airport einen Heli + Flugzeug. +// Jedes Paar bekommt einen individuellen Phase-Offset, damit Helis +// und Flugzeuge verschiedener Flughäfen nicht synchron laufen. +const airportVehicles = new Map(); // airport-i → { heli, plane, idx, phaseOffset } + +function syncAirportVehicles() { const airport = state.ownedMeasures && state.ownedMeasures.airport; - const hasAirport = airport && airport.count > 0 && airport.instances && airport.instances[0]; - heli.visible = !!hasAirport; - if (!hasAirport) return; - const inst = airport.instances[0]; - if (!inst.pos) return; + const keep = new Set(); + if (airport && airport.instances) { + airport.instances.forEach((inst, idx) => { + const key = instanceKey('airport', idx); + keep.add(key); + if (airportVehicles.has(key)) return; + const h = makeHeli(); + h.userData.infoKey = 'heli_info'; + h.visible = false; + ambientGroup.add(h); + const p = makePlane(); + p.userData.infoKey = 'aviation'; + p.visible = false; + ambientGroup.add(p); + airportVehicles.set(key, { heli: h, plane: p, idx, phaseOffset: idx * 17 }); + }); + } + for (const key of Array.from(airportVehicles.keys())) { + if (keep.has(key)) continue; + const v = airportVehicles.get(key); + ambientGroup.remove(v.heli); ambientGroup.remove(v.plane); + airportVehicles.delete(key); + } +} - // Airport-Mesh aus placedMeshes holen (Rotation beachten); fallback auf inst.pos - const airportMesh = placedMeshes.get(instanceKey('airport', 0)); - const airportPos = airportMesh ? airportMesh.position : new THREE.Vector3(inst.pos.x, inst.pos.y, inst.pos.z); - const airportRotY = airportMesh ? airportMesh.rotation.y : (inst.rotation || 0); - const padLocal = { x: 0.85 * 2, y: 0.04 * 2, z: 0.55 * 2 }; // inkl. 2x-Scale - const cosR = Math.cos(airportRotY), sinR = Math.sin(airportRotY); - const padWorld = { - x: airportPos.x + padLocal.x * cosR - padLocal.z * sinR, - y: airportPos.y + padLocal.y, - z: airportPos.z + padLocal.x * sinR + padLocal.z * cosR, +const HELI_CYCLE = 42; +const PLANE_CYCLE = 110; + +function updateAirportVehicles(tNow) { + const airport = state.ownedMeasures && state.ownedMeasures.airport; + if (!airport) return; + for (const [key, v] of airportVehicles) { + const inst = airport.instances && airport.instances[v.idx]; + if (!inst || !inst.pos) { v.heli.visible = false; v.plane.visible = false; continue; } + const airportMesh = placedMeshes.get(key); + if (!airportMesh) continue; + updateSingleHeli(v, tNow, airportMesh); + updateSinglePlane(v, tNow, airportMesh); + } +} + +// Lokale→Welt-Transformation für Airport-relative Punkte (inkl. Airport-Rotation) +function airportLocalToWorld(airportMesh, lx, ly, lz) { + const cosR = Math.cos(airportMesh.rotation.y); + const sinR = Math.sin(airportMesh.rotation.y); + return { + x: airportMesh.position.x + lx * cosR - lz * sinR, + y: airportMesh.position.y + ly, + z: airportMesh.position.z + lx * sinR + lz * cosR, }; +} - const cycleT = tNow % HELI_CYCLE; - let rotorSpeed = 18; - if (cycleT < 20) { - // Orbit-Phase: kreist über Airport-Zentrum - const a = tNow * 0.45; - heli.position.set( - airportPos.x + Math.cos(a) * 3.5, - airportPos.y + 2.8 + Math.sin(tNow * 0.9) * 0.15, - airportPos.z + Math.sin(a) * 3.5 - ); - heli.rotation.y = a + Math.PI / 2; - } else if (cycleT < 24) { - // Landung: linearer Abstieg vom Orbit-Punkt zum Pad - const t = (cycleT - 20) / 4; // 0..1 - const a = 20 * 0.45; // Endphase des Orbits - const startX = airportPos.x + Math.cos(a) * 3.5; - const startZ = airportPos.z + Math.sin(a) * 3.5; - const startY = airportPos.y + 2.8; +function updateSingleHeli(v, tNow, airportMesh) { + const heli = v.heli; + heli.visible = true; + // Heli-Pad-Offset aus dem Airport-Mesh (inkl. LIFT-Korrektur), × Scale 2 + const padLocalRaw = airportMesh.userData.heliPadLocal || { x: 0.85, y: 0.28, z: 0.55 }; + const padLocal = { x: padLocalRaw.x * 2, y: padLocalRaw.y * 2, z: padLocalRaw.z * 2 }; + const padWorld = airportLocalToWorld(airportMesh, padLocal.x, padLocal.y, padLocal.z); + const rotY = airportMesh.rotation.y; + + const tPhase = tNow + v.phaseOffset; + const cycleT = tPhase % HELI_CYCLE; + const cycleNum = Math.floor(tPhase / HELI_CYCLE); + // Alle ~3 Zyklen macht der Heli statt der üblichen Airport-Runde eine + // größere Runde um den Vulkan (VOLCANO_X,VOLCANO_Z). Deterministisch + // abhängig von cycleNum + phaseOffset → synchron über Reloads. + const volcanoTour = (cycleNum % 3) === (Math.floor(v.phaseOffset) % 3); + let rotorSpeed = 20; + + if (cycleT < 24) { + // Orbit-Phase: je nach Modus Airport-Kreis oder Vulkan-Runde + const a = tPhase * 0.45; + if (volcanoTour) { + // Großer Bogen: Zentrum Vulkan, Radius 9, Höhe steigt leicht + const cx = VOLCANO_X, cz = VOLCANO_Z; + heli.position.set( + cx + Math.cos(a) * 9, + airportMesh.position.y + 5.5 + Math.sin(tPhase * 0.8) * 0.3, + cz + Math.sin(a) * 9 + ); + heli.rotation.y = a + Math.PI / 2; + } else { + heli.position.set( + airportMesh.position.x + Math.cos(a) * 3.5, + airportMesh.position.y + 2.8 + Math.sin(tPhase * 0.9) * 0.15, + airportMesh.position.z + Math.sin(a) * 3.5 + ); + heli.rotation.y = a + Math.PI / 2; + } + } else if (cycleT < 28) { + // Landung: linear zurück zum Pad + const t = (cycleT - 24) / 4; + const a = 24 * 0.45; + const startX = volcanoTour ? VOLCANO_X + Math.cos(a) * 9 : airportMesh.position.x + Math.cos(a) * 3.5; + const startZ = volcanoTour ? VOLCANO_Z + Math.sin(a) * 9 : airportMesh.position.z + Math.sin(a) * 3.5; + const startY = airportMesh.position.y + (volcanoTour ? 5.5 : 2.8); heli.position.set( lerp(startX, padWorld.x, t), - lerp(startY, padWorld.y + 0.22, t), // Skids-Offset + lerp(startY, padWorld.y + 0.22, t), lerp(startZ, padWorld.z, t) ); heli.rotation.y = a + Math.PI / 2; - } else if (cycleT < 32) { - // Sitzt auf dem Pad — Rotor läuft langsamer aus und wieder an + } else if (cycleT < 37) { + // Sitzt auf dem Pad — Rotor läuft aus und wieder an heli.position.set(padWorld.x, padWorld.y + 0.22, padWorld.z); - heli.rotation.y = airportRotY + Math.PI / 2; - const sitT = cycleT - 24; // 0..8 - rotorSpeed = sitT < 2 ? 18 * (1 - sitT / 2) : (sitT > 6 ? 18 * (sitT - 6) / 2 : 0); + heli.rotation.y = rotY + Math.PI / 2; + const sitT = cycleT - 28; + rotorSpeed = sitT < 2 ? 20 * (1 - sitT / 2) : (sitT > 7 ? 20 * (sitT - 7) / 2 : 0); } else { - // Takeoff: Aufstieg vom Pad in den Orbit-Startpunkt - const t = (cycleT - 32) / 4; // 0..1 - const a = (HELI_CYCLE) * 0.45; // nächster Orbit-Start - const endX = airportPos.x + Math.cos(a) * 3.5; - const endZ = airportPos.z + Math.sin(a) * 3.5; - const endY = airportPos.y + 2.8; + // Takeoff zurück in den Orbit-Startpunkt + const t = (cycleT - 37) / (HELI_CYCLE - 37); + const a = HELI_CYCLE * 0.45; + const endX = airportMesh.position.x + Math.cos(a) * 3.5; + const endZ = airportMesh.position.z + Math.sin(a) * 3.5; + const endY = airportMesh.position.y + 2.8; heli.position.set( lerp(padWorld.x, endX, t), lerp(padWorld.y + 0.22, endY, t), lerp(padWorld.z, endZ, t) ); - heli.rotation.y = airportRotY + Math.PI / 2; + heli.rotation.y = rotY + Math.PI / 2; + } + const r = heli.userData.rotorRef; + if (r) r.rotation.y = tPhase * rotorSpeed; +} + +function updateSinglePlane(v, tNow, airportMesh) { + const plane = v.plane; + const rotY = airportMesh.rotation.y; + const RUNWAY_END = 2.2; // lokal, nach 2x-Scale also 4.4 in Welt + const RUNWAY_Y = 0.50; // auf Plateau-Oberkante (LIFT+Scale²≈0.26) + + // Zyklus-Phase (Sekunden) mit individuellem Offset pro Airport, plus + // +60 s Grund-Offset damit Plane nicht sofort beim Level-Start startet. + const tPhase = (tNow + v.phaseOffset + 60) % PLANE_CYCLE; + plane.visible = true; + const prop = plane.userData.propRef; + + // Hilfsfunktion: Plane an lokal (lx, ly, lz) mit Rotation-Offset um Y + function place(lx, ly, lz, pitch, extraYaw) { + const w = airportLocalToWorld(airportMesh, lx, ly, lz); + plane.position.set(w.x, w.y, w.z); + // Basis-Yaw = airportRotY + extraYaw (π für Rollen nach -X, 0 für +X) + plane.rotation.set(pitch || 0, rotY + (extraYaw || 0), 0); } - const r = heli.userData.rotorRef; - if (r) r.rotation.y = tNow * rotorSpeed; + if (tPhase < 14) { + // Parken am Runway-Anfang (+X-Ende lokal), Nase zeigt nach Startrichtung (-X) + place(RUNWAY_END - 0.3, RUNWAY_Y, 0, 0, Math.PI); + } else if (tPhase < 18) { + // Takeoff-Roll: rollt von +X nach -X, hebt am Ende leicht ab + const t = (tPhase - 14) / 4; + const lx = (RUNWAY_END - 0.3) - t * (2 * RUNWAY_END - 0.6); + const ly = RUNWAY_Y + (t > 0.75 ? (t - 0.75) * 0.6 : 0); + place(lx, ly, 0, -t * 0.2, Math.PI); + } else if (tPhase < 26) { + // Climb-Out: steigt auf, entfernt sich weiter in -X-lokal + const t = (tPhase - 18) / 8; + const lx = -(RUNWAY_END - 0.3) - t * 18; + const ly = RUNWAY_Y + 0.15 + t * 4.0; + place(lx, ly, 0, -0.28, Math.PI); + } else if (tPhase < 80) { + // Unterwegs — unsichtbar (kommt von weit draußen zurück) + plane.visible = false; + } else if (tPhase < 90) { + // Approach: kommt in -X-lokal auf die Landung zu, sinkt + const t = (tPhase - 80) / 10; + const lx = -22 + t * (22 - (RUNWAY_END + 0.3)); + const ly = 4.2 - t * 3.7; // 4.2 → 0.5 + place(lx, ly, 0, 0.15 - t * 0.15, Math.PI); + } else if (tPhase < 94) { + // Landing-Roll: von -X nach +X mit leichtem Pitch-Up am Anfang, dann flach + const t = (tPhase - 90) / 4; + const lx = -(RUNWAY_END - 0.3) + t * (2 * RUNWAY_END - 0.6); + place(lx, RUNWAY_Y, 0, 0, Math.PI); + } else { + // Taxi + parken (zurück in Startposition) + place(RUNWAY_END - 0.3, RUNWAY_Y, 0, 0, Math.PI); + } + if (prop && plane.visible) prop.rotation.x = tPhase * 22; } // --- Start-Dorf: 10 Haus-Positionen auf der Insel, Sichtbarkeit skaliert mit Bevölkerung --- @@ -4146,10 +4328,13 @@ const clouds = []; for (let i = 0; i < 4; i++) { const c = makeCloud(); c.userData.phase = Math.random(); // 0..1 Startverschiebung - c.userData.height = 13 + Math.random() * 4; // y + // Höhe nahe Vulkan-Spitze (Terrain ≈ 5.2 + Dome 2.1 ≈ 7.3) — Wolken + // ziehen knapp über die Gletscherkuppe, statt hoch am Himmel zu + // schweben. Fühlbarer "Dunst um den Vulkan"-Look. + c.userData.height = 6.5 + Math.random() * 1.8; c.userData.zStart = 4 + Math.random() * 6; // Insel-Vorder-Bereich c.userData.zEnd = -16 - Math.random() * 4; // hinterm Vulkan - c.userData.scale = 0.8 + Math.random() * 0.5; + c.userData.scale = 0.75 + Math.random() * 0.45; c.userData.speed = 0.8 + Math.random() * 0.6; c.scale.setScalar(c.userData.scale); ambientGroup.add(c); @@ -4276,10 +4461,56 @@ function handleTreesFelled(count, currCount) { showEvent('🪓', msg, 'bad', 'blackout'); } +// --- Flut-Tönung --- +// Sobald der Ozean über einem Mesh-Fußpunkt steht, lerpen wir dessen +// Material-Farben Richtung Dunkelgrau (defekter, schwarzer Look) und +// kippen das Mesh leicht nach hinten (collapsing). Die Original-Farben +// werden lazy beim ersten Flood-Check in userData.origColors gecacht. +const FLOOD_DEEP = 0.55; // Welt-Einheiten unter Wasserlinie für volle Tönung +const FLOOD_DARK = { r: 0.12, g: 0.12, b: 0.12 }; + +function cacheOriginalColors(mesh) { + if (mesh.userData.origColors) return; + const cache = []; + mesh.traverse((obj) => { + if (obj.isMesh && obj.material && obj.material.color) { + cache.push({ mat: obj.material, c: obj.material.color.clone() }); + } + }); + mesh.userData.origColors = cache; + mesh.userData.origRotX = mesh.rotation.x || 0; +} + +function tintMeshFlood(mesh, floodT) { + cacheOriginalColors(mesh); + for (const { mat, c } of mesh.userData.origColors) { + mat.color.setRGB( + c.r + (FLOOD_DARK.r - c.r) * floodT, + c.g + (FLOOD_DARK.g - c.g) * floodT, + c.b + (FLOOD_DARK.b - c.b) * floodT + ); + } + // Leichter Kipp nach hinten, bis −0.12 rad (≈ 7°) + mesh.rotation.x = mesh.userData.origRotX - floodT * 0.12; +} + +function applyFloodTintAll() { + const waterY = ocean.position.y; + const targets = []; + for (const mesh of placedMeshes.values()) targets.push(mesh); + for (const house of villageHouses) if (house.visible) targets.push(house); + if (citizenBeach) targets.push(citizenBeach); + for (const m of targets) { + const footY = m.position.y; + const floodT = Math.max(0, Math.min(1, (waterY - footY) / FLOOD_DEEP)); + tintMeshFlood(m, floodT); + } +} + // Zentrale Ambient-Update-Funktion (aus sceneRender pro Frame) function updateAmbient(tNow, dt) { updateBoats(tNow); - updateHeli(tNow); + updateAirportVehicles(tNow); updateVillage(tNow, dt); updateClouds(tNow); updateFallenTrees(dt); @@ -4662,8 +4893,20 @@ function showEndScreen(stars, score, reason) { const achievedCount = goals.filter(g => g.achieved).length; const totalCount = goals.length; - $('#end-icon').textContent = won ? '🏆' : (reason === 'pleite' ? '💸' : '🌊'); - $('#end-title').textContent = won ? 'Ziel erreicht!' : (reason === 'pleite' ? 'Budget aufgebraucht' : (reason === 'ueberflutet' ? 'Küste überflutet' : 'Ziel nicht erreicht')); + const icon = won ? '🏆' + : reason === 'pleite' ? '💸' + : reason === 'ueberflutet' ? '🌊' + : reason === 'co2-top' ? '🌫' + : reason === 'temp-top' ? '🌡' + : '⚠️'; + const title = won ? 'Ziel erreicht!' + : reason === 'pleite' ? 'Budget aufgebraucht' + : reason === 'ueberflutet' ? 'Küste überflutet' + : reason === 'co2-top' ? 'CO₂-Gefahrenzone erreicht' + : reason === 'temp-top' ? 'Klima kippt — zu heiß' + : 'Ziel nicht erreicht'; + $('#end-icon').textContent = icon; + $('#end-title').textContent = title; $('#end-subtitle').textContent = won ? ('Du hast bis ' + (state.startYear + state.tick) + ' alle ' + totalCount + ' Hauptziele erreicht. Hervorragende Leistung!') : ('Im Jahr ' + (state.startYear + state.tick) + ' ist der Durchgang beendet — ' + achievedCount + ' von ' + totalCount + ' Zielen erreicht.');