diff --git a/App/sims/klima/game-3d.html b/App/sims/klima/game-3d.html
index 8abc903..c305796 100644
--- a/App/sims/klima/game-3d.html
+++ b/App/sims/klima/game-3d.html
@@ -1574,6 +1574,11 @@ function finalizeBuy(id, hitPos) {
if (id === 'green-roof') {
showEvent('đĄ', 'GrĂŒndach fertig â neue Wohnungen frei. Einwohner:innen und Touristen können zuziehen, das bringt mehr Steuer-Einnahmen.', 'good', 'green_roof_info');
}
+ // Solar-Hinweis: Kauf-Feedback mit Strom-Leistung und Wartung, damit
+ // SchĂŒler:innen die beiden Kennzahlen direkt mitnehmen.
+ if (id === 'solar') {
+ showEvent('âïž', 'Solaranlage fertig â +1 MW sauberer Strom. Wartung: 2 Mio âŹ/Jahr.', 'good', 'solar_info');
+ }
// Ketten-Bau: Placement-Mode direkt mit gleicher MaĂnahme fortsetzen,
// solange das Budget noch reicht. Der User beendet bewusst mit ESC /
@@ -1614,7 +1619,13 @@ function eventFiredOnce(id) {
* die View reicht das an Toast, Sound, Server und Overlays weiter.
*/
function simulateTick() {
+ // Vor dem Tick: Bestand der Forest-Instanzen merken, damit wir nach dem
+ // Tick erkennen, wie viele BÀume von der Blackout-Logik gefÀllt wurden.
+ const prevForestCount = (state.ownedMeasures.forest && state.ownedMeasures.forest.count) || 0;
const res = KlimaEngine.tick(state);
+ const currForestCount = (state.ownedMeasures.forest && state.ownedMeasures.forest.count) || 0;
+ const felled = prevForestCount - currForestCount;
+ if (felled > 0) handleTreesFelled(felled, currForestCount);
// ZusÀtzlicher Drama-Sound bei +2 °C (Engine pusht nur den Text-Event)
for (const ev of res.newEvents) {
@@ -4158,12 +4169,120 @@ function updateClouds(tNow) {
}
}
+// --- GefÀllte BÀume (Blackout-Animation) ---
+// Statt des harten Verschwindens beim Stromausfall kippt der Baum langsam
+// um, ein Totenkopf erscheint kurz darauf ĂŒber ihm, danach fadet alles
+// weg. Die Animations-Zeit hĂ€ngt an state.animMs â Pause friert ein.
+const fallenTrees = []; // [{ mesh, mats, age, skull, skullMat }]
+let _skullTextureCache = null;
+function getSkullTexture() {
+ if (_skullTextureCache) return _skullTextureCache;
+ const size = 128;
+ const canvas = document.createElement('canvas');
+ canvas.width = canvas.height = size;
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, size, size);
+ ctx.font = 'bold 96px "Apple Color Emoji", "Segoe UI Emoji", sans-serif';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText('â ïž', size / 2, size / 2 + 4);
+ _skullTextureCache = new THREE.CanvasTexture(canvas);
+ _skullTextureCache.needsUpdate = true;
+ return _skullTextureCache;
+}
+
+/** Zieht einen Baum-Mesh aus `placedMeshes` heraus und startet die
+ * Fell-Animation. Der Mesh bleibt in `measuresGroup` hÀngen, wird aber
+ * nicht mehr vom nÀchsten `syncMeasureMeshes`-Durchlauf angefasst. */
+function startTreeFall(mesh) {
+ const mats = [];
+ mesh.traverse((obj) => {
+ if (obj.isMesh && obj.material) {
+ const clone = obj.material.clone();
+ clone.transparent = true;
+ obj.material = clone;
+ mats.push(clone);
+ }
+ });
+ fallenTrees.push({ mesh, mats, age: 0, skull: null, skullMat: null });
+}
+
+function updateFallenTrees(dt) {
+ const FALL_DUR = 1.4;
+ const SKULL_DELAY = 1.8;
+ const SKULL_FADE = 0.4;
+ const FADE_DELAY = 3.2;
+ const FADE_DUR = 2.0;
+ for (let i = fallenTrees.length - 1; i >= 0; i--) {
+ const f = fallenTrees[i];
+ f.age += dt;
+ // Phase 1: Baum kippt zur Seite (easeOutQuad)
+ const fallT = Math.min(1, f.age / FALL_DUR);
+ f.mesh.rotation.z = (1 - Math.pow(1 - fallT, 2)) * (-Math.PI / 2.05);
+ // Phase 2: Totenkopf spawnt verzögert, fadet sanft ein
+ if (!f.skull && f.age >= SKULL_DELAY) {
+ const skullMat = new THREE.MeshBasicMaterial({
+ map: getSkullTexture(), transparent: true, opacity: 0,
+ side: THREE.DoubleSide, depthWrite: false, depthTest: false,
+ });
+ const skull = new THREE.Mesh(new THREE.PlaneGeometry(0.6, 0.6), skullMat);
+ skull.position.set(
+ f.mesh.position.x,
+ f.mesh.position.y + 0.9,
+ f.mesh.position.z
+ );
+ skull.renderOrder = 10;
+ scene.add(skull);
+ f.skull = skull;
+ f.skullMat = skullMat;
+ }
+ if (f.skullMat) {
+ // Skull schaut immer zur Kamera (Billboard)
+ f.skull.quaternion.copy(camera.quaternion);
+ const t1 = Math.max(0, Math.min(1, (f.age - SKULL_DELAY) / SKULL_FADE));
+ if (f.age < FADE_DELAY) f.skullMat.opacity = t1 * 0.92;
+ }
+ // Phase 3: Baum + Skull faden raus
+ if (f.age > FADE_DELAY) {
+ const fadeT = Math.min(1, (f.age - FADE_DELAY) / FADE_DUR);
+ const op = 1 - fadeT;
+ for (const m of f.mats) m.opacity = op;
+ if (f.skullMat) f.skullMat.opacity = op * 0.92;
+ if (f.age > FADE_DELAY + FADE_DUR + 0.05) {
+ measuresGroup.remove(f.mesh);
+ for (const m of f.mats) m.dispose && m.dispose();
+ if (f.skull) { scene.remove(f.skull); f.skullMat.dispose(); }
+ fallenTrees.splice(i, 1);
+ }
+ }
+ }
+}
+
+/** Wird aus simulateTick gerufen, wenn die Engine `felled` BĂ€ume gepopped
+ * hat. Findet die jetzt ĂŒberzĂ€hligen Forest-Meshes (Index â„ currCount),
+ * zieht sie aus placedMeshes raus und ĂŒbergibt sie der Fall-Animation.
+ * Zeigt eine Toast-Card pro Tick, nicht pro Baum (sonst spam). */
+function handleTreesFelled(count, currCount) {
+ for (const [key, mesh] of Array.from(placedMeshes)) {
+ if (!key.startsWith('forest-')) continue;
+ const idx = parseInt(key.slice(7), 10);
+ if (idx < currCount) continue;
+ placedMeshes.delete(key);
+ startTreeFall(mesh);
+ }
+ const msg = count > 1
+ ? 'Zu wenig Strom! Die Bewohner:innen fÀllen ' + count + ' BÀume zum Heizen.'
+ : 'Zu wenig Strom! Die Bewohner:innen fÀllen einen Baum zum Heizen.';
+ showEvent('đȘ', msg, 'bad', 'blackout');
+}
+
// Zentrale Ambient-Update-Funktion (aus sceneRender pro Frame)
function updateAmbient(tNow, dt) {
updateBoats(tNow);
updateHeli(tNow);
updateVillage(tNow, dt);
updateClouds(tNow);
+ updateFallenTrees(dt);
}
/* ============================================================