diff --git a/App/sims/klima/game-3d.html b/App/sims/klima/game-3d.html index f8e635e..295b45a 100644 --- a/App/sims/klima/game-3d.html +++ b/App/sims/klima/game-3d.html @@ -1540,14 +1540,11 @@ function buyMeasure(id) { function finalizeBuy(id, hitPos) { let rotation = +placementRotation.toFixed(3); - // Flughafen-Ausrichtung: Runway (lokales +X = Start-Richtung) zeigt - // IMMER vom Vulkan weg, damit Starts und Landungen in den offenen Ozean - // gehen und nicht in die Bergflanke. Überschreibt die zufällige - // placementRotation für Airports. + // Flughafen-Ausrichtung: Runway parallel zur Insel-Küste an der Airport- + // Position (coastRotation). So fliegen Start und Landung immer parallel + // am Vulkan vorbei statt direkt auf ihn zu oder von ihm weg. if (id === 'airport') { - const dx = hitPos.x - VOLCANO_X; - const dz = hitPos.z - VOLCANO_Z; - rotation = +Math.atan2(-dz, dx).toFixed(3); + rotation = +coastRotation(hitPos.x, hitPos.z).toFixed(3); } const zone = MEASURE_ZONES_3D[id]; const zoneId = zone ? zone.kind : 'land'; @@ -3117,12 +3114,11 @@ function syncMeasureMeshes() { const p = inst.pos || { x: 0, y: 0, z: 0 }; mesh.position.set(p.x, p.y, p.z); if (typeof inst.rotation === 'number') mesh.rotation.y = inst.rotation; - // Airport: Runway-Ausrichtung IMMER vom Vulkan weg erzwingen - // (greift auch bei alten Saves mit zufälliger Rotation). + // Airport: Runway-Ausrichtung parallel zur Küste an der Airport- + // Position erzwingen (greift auch bei alten Saves). Flug geht damit + // seitlich am Vulkan vorbei statt durch ihn. if (id === 'airport') { - const dx = p.x - VOLCANO_X; - const dz = p.z - VOLCANO_Z; - mesh.rotation.y = Math.atan2(-dz, dx); + mesh.rotation.y = coastRotation(p.x, p.z); inst.rotation = mesh.rotation.y; } measuresGroup.add(mesh); @@ -4094,56 +4090,63 @@ function updateSinglePlane(v, tNow, airportMesh) { const plane = v.plane; const rotY = airportMesh.rotation.y; const RUNWAY_END = 2.2; - const RUNWAY_Y = 0.50; + // Runway-Oberkante = airport.y + 0.56 (Scale 2, Plateau + Runway-Mesh). + // Plane-Räder sitzen bei -0.22 unter plane.position.y → Parkhöhe 0.78, + // damit die Räder exakt auf dem Asphalt stehen. + const RUNWAY_Y = 0.78; const tPhase = (tNow + v.phaseOffset) % PLANE_CYCLE; plane.visible = true; const prop = plane.userData.propRef; - // place: Nase zeigt IMMER in Airport-lokales +X (= Start- und Lande- - // Richtung, vom Vulkan weg). Banking via roll (rotation.z). - function place(lx, ly, lz, pitch, roll) { + // Nose-Richtung über extraYaw: 0 = Airport-lokales +X (Takeoff), π = -X + // (Landung/Anflug aus der anderen Richtung). Roll via rotation.z. + function place(lx, ly, lz, pitch, roll, yaw) { const w = airportLocalToWorld(airportMesh, lx, ly, lz); plane.position.set(w.x, w.y, w.z); - plane.rotation.set(pitch || 0, rotY, roll || 0); + plane.rotation.set(pitch || 0, rotY + (yaw || 0), roll || 0); } if (tPhase < 14) { - // Parken am Runway-Anfang (-X-Ende), Nase zeigt +X - place(-(RUNWAY_END - 0.3), RUNWAY_Y, 0, 0, 0); + // Parken am Runway-Anfang (-X-Ende), Nase +X, bereit für Takeoff + place(-(RUNWAY_END - 0.3), RUNWAY_Y, 0, 0, 0, 0); } else if (tPhase < 18) { - // Takeoff-Roll: -X → +X, am Ende leicht abheben + // Takeoff-Roll: -X → +X, leicht abheben am Ende 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, 0); + place(lx, ly, 0, -t * 0.2, 0, 0); } else if (tPhase < 26) { - // Climb-Out: weiter +X, steigt auf, ab t=0.5 bankt es leicht für die - // Kurve (rotation.z Roll, sichtbar seitwärts kippend vorm Abtauchen) + // Climb-Out: weiter +X, steigt auf, Banking am Ende in die Kurve const t = (tPhase - 18) / 8; const lx = (RUNWAY_END - 0.3) + t * 18; const ly = RUNWAY_Y + 0.15 + t * 4.0; const roll = t > 0.5 ? (t - 0.5) * 0.9 : 0; - place(lx, ly, 0, -0.28, roll); + place(lx, ly, 0, -0.28, roll, 0); } else if (tPhase < 76) { - // Unterwegs — unsichtbar (fliegt irgendwo in der Ferne den Bogen) + // Unterwegs — unsichtbar (fliegt irgendwo den Bogen) plane.visible = false; } else if (tPhase < 84) { - // Approach: von -X-Ferne zurück zum Landeanflug, Banking löst sich - // langsam auf (Kurveneinleitung rückwärts interpretiert) + // Approach: aus +X-Ferne kommend, Nase zeigt -X (Landerichtung), + // Banking löst sich auf. const t = (tPhase - 76) / 8; - const lx = -22 + t * (22 - (RUNWAY_END + 0.3)); // -22 → -2.5 + const lx = 22 - t * (22 - (RUNWAY_END + 0.3)); // +22 → +2.5 const ly = 4.2 - t * 3.7; - const roll = t < 0.5 ? -(0.5 - t) * 0.9 : 0; // Linkskurven-Roll raus - place(lx, ly, 0, 0.15 - t * 0.15, roll); + const roll = t < 0.5 ? -(0.5 - t) * 0.9 : 0; + place(lx, ly, 0, 0.15 - t * 0.15, roll, Math.PI); } else if (tPhase < 88) { - // Landing-Roll: -X-Ende → +X-Ende (selbe Richtung wie Takeoff) + // Landing-Roll: +X → -X, Nase -X (Bewegung und Nase passen) const t = (tPhase - 84) / 4; - const lx = -(RUNWAY_END - 0.3) + t * (2 * RUNWAY_END - 0.6); - place(lx, RUNWAY_Y, 0, 0, 0); + const lx = (RUNWAY_END - 0.3) - t * (2 * RUNWAY_END - 0.6); + place(lx, RUNWAY_Y, 0, 0, 0, Math.PI); + } else if (tPhase < 92) { + // Wenden am -X-Ende: Nase-Rotation interpoliert von π zurück auf 0 + const t = (tPhase - 88) / 4; + const yaw = Math.PI * (1 - t); + place(-(RUNWAY_END - 0.3), RUNWAY_Y, 0, 0, 0, yaw); } else { - // Taxi zurück zur Parkposition — unsichtbar (Teleport OK, kurz) - plane.visible = false; + // Parken am -X-Ende, Nase +X — bleibt bis zum nächsten Zyklus + place(-(RUNWAY_END - 0.3), RUNWAY_Y, 0, 0, 0, 0); } if (prop && plane.visible) prop.rotation.x = tPhase * 22; }