From 2b4a92e614ae369451896699b1fd2ce1fd60b4e7 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 11 Jul 2026 09:13:09 +0200 Subject: [PATCH] Tourismustal: Heli hebt sichtbar ab und landet am Pad Senkrechter Start/Landung mit geglaetteter Hoehenkurve, Rotor laeuft an/aus (spin-basiert statt Echtzeit), Rotor-Abwind-Wirbel am Boden, Schatten skaliert mit Flughoehe. Kein Teleport-Erscheinen mehr. Co-Authored-By: Claude Opus 4.8 (1M context) --- App/sims/tourismustal/js/iso.js | 51 ++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/App/sims/tourismustal/js/iso.js b/App/sims/tourismustal/js/iso.js index 0ca24d4..6187a55 100644 --- a/App/sims/tourismustal/js/iso.js +++ b/App/sims/tourismustal/js/iso.js @@ -652,21 +652,30 @@ export class IsoRenderer { if (victim) { const st = s.buildings.find(b => b.type === 'rescue'); const home = { x: st.c + 0.5, y: st.r - 0.5 }; - this.heli = { x: home.x, y: home.y, phase: 'out', face: 1, - bob: 0, hoverT: 0, ang: 0, victim, home }; + // Start am Boden: erst Rotor hochdrehen und senkrecht abheben + this.heli = { x: home.x, y: home.y, phase: 'takeoff', face: 1, + bob: 0, hoverT: 0, ang: 0, alt: 0, spin: 0, victim, home }; } } - // Heli langsam & natürlich: hinfliegen → über dem Gast kreisen → zurück + // Heli natürlich: abheben → hinfliegen → kreisen → zurück → landen if (this.heli) { const h = this.heli; h.bob += dt; + // Rotor: dreht am Boden langsam, in der Luft schnell (Anlauf/Auslauf) + h.spin += dt * (2 + 14 * (h.alt ?? 1)); const SPD = 2.6 * dt; // gemächliches Tempo // Nase nach der BILDSCHIRM-Bewegung ausrichten: screen-x ∝ (Spalte − Reihe) const faceBy = (px, py) => { const sdx = (h.x - px) - (h.y - py); if (Math.abs(sdx) > 0.0015) h.face = sdx > 0 ? 1 : -1; }; - if (h.phase === 'hover') { + if (h.phase === 'takeoff') { + h.alt = Math.min(1, h.alt + dt / 1.4); // senkrecht abheben + if (h.alt >= 1) h.phase = 'out'; + } else if (h.phase === 'landing') { + h.alt = Math.max(0, h.alt - dt / 1.6); // sanft aufsetzen + if (h.alt <= 0) this.heli = null; // steht wieder am Pad + } else if (h.phase === 'hover') { h.hoverT += dt; h.ang += dt * 1.6; const px = h.x, py = h.y; h.x = h.victim.x + Math.cos(h.ang) * 1.1; // kreist über dem Gast @@ -678,7 +687,7 @@ export class IsoRenderer { const dx = t.x - h.x, dy = t.y - h.y, dist = Math.hypot(dx, dy); if (dist < (h.phase === 'out' ? 1.1 : 0.3)) { if (h.phase === 'out') { h.phase = 'hover'; h.hoverT = 0; } - else this.heli = null; // an der Station gelandet + else { h.phase = 'landing'; h.x = t.x; h.y = t.y; } // über dem Pad einschweben } else { const px = h.x, py = h.y; h.x += (dx / dist) * SPD; h.y += (dy / dist) * SPD; @@ -692,10 +701,27 @@ export class IsoRenderer { if (!this.heli) return; const ctx = this.ctx; const h = this.heli; - const fly = 9 + Math.sin(h.bob * 3) * 0.35; // sanftes Wippen + const lift = h.alt ?? 1; // 0 = Boden, 1 = Flughöhe + // Wippen erst in der Luft; eased Höhenkurve für sanftes Abheben/Aufsetzen + const ease = lift * lift * (3 - 2 * lift); + const fly = 0.6 + ease * (8.4 + Math.sin(h.bob * 3) * 0.35 * lift); const { x, y } = this.tileToWorld(h.x, h.y, fly); const g = this.tileToWorld(h.x, h.y, this.elevAt(h.x, h.y)); - this._shadow(g.x, g.y, 9, 3.6); + // Schatten: am Boden groß und satt, in der Luft kleiner und blasser + this._shadow(g.x, g.y, 9 - 2.5 * ease, 3.6 - ease); + // Rotor-Abwind beim Starten/Landen: Wirbel-Ringe am Boden + if (lift > 0.02 && lift < 0.85 && (h.phase === 'takeoff' || h.phase === 'landing')) { + const a = (1 - lift) * 0.4; + ctx.strokeStyle = `rgba(228,224,212,${a.toFixed(2)})`; + ctx.lineWidth = 1.6; + for (let i = 0; i < 3; i++) { + const p = (h.bob * 1.8 + i / 3) % 1; + ctx.beginPath(); + ctx.ellipse(g.x, g.y + 1, 6 + p * 16, 2.4 + p * 6, 0, 0.3 + i, 2.4 + i); + ctx.stroke(); + } + ctx.lineWidth = 1; + } const f = h.face || 1; // 1 = Nase nach rechts // Heckausleger (hinten) + Finne + drehender Heckrotor @@ -703,7 +729,7 @@ export class IsoRenderer { ctx.beginPath(); ctx.moveTo(x - 3 * f, y - 1); ctx.lineTo(x - 22 * f, y - 4); ctx.stroke(); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x - 22 * f, y - 4); ctx.lineTo(x - 24 * f, y - 9); ctx.stroke(); - const trot = this.time / 25; + const trot = h.spin * 2.2; ctx.strokeStyle = 'rgba(47,62,70,0.6)'; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(x - 24 * f - Math.cos(trot) * 4, y - 7 - Math.sin(trot) * 4); @@ -740,9 +766,12 @@ export class IsoRenderer { ctx.beginPath(); ctx.moveTo(x, y - 6.5); ctx.lineTo(x, y - 10); ctx.stroke(); ctx.fillStyle = '#2f3e46'; ctx.beginPath(); ctx.arc(x, y - 10, 1.6, 0, 7); ctx.fill(); - ctx.strokeStyle = 'rgba(47,62,70,0.12)'; ctx.lineWidth = 3; - ctx.beginPath(); ctx.ellipse(x, y - 10, 20, 5, 0, 0, 7); ctx.stroke(); - const rot = this.time / 55; + // Rotor-Unschärfe-Scheibe nur bei schneller Drehung (in der Luft) + if (lift > 0.4) { + ctx.strokeStyle = `rgba(47,62,70,${(0.12 * lift).toFixed(2)})`; ctx.lineWidth = 3; + ctx.beginPath(); ctx.ellipse(x, y - 10, 20, 5, 0, 0, 7); ctx.stroke(); + } + const rot = h.spin; ctx.strokeStyle = 'rgba(47,62,70,0.55)'; ctx.lineWidth = 2; for (let i = 0; i < 2; i++) { const a = rot + i * Math.PI;