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) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:13:09 +02:00
parent 7453bebbc3
commit 2b4a92e614
+40 -11
View File
@@ -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;