Klima 3D: Heli pro Airport, Flugzeug, Flut-Tönung, Graph-Top-Verlust
- 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) <noreply@anthropic.com>
This commit is contained in:
+305
-62
@@ -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.');
|
||||
|
||||
Reference in New Issue
Block a user