From c7b3636815f600693c15e5b9d400f6f8aebdb5c1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 19 Apr 2026 14:01:13 +0200 Subject: [PATCH] =?UTF-8?q?Klima=203D:=20Baumf=C3=A4ll-Animation=20+=20Sol?= =?UTF-8?q?ar-Kauf-Hinweis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Blackout fällt Bäume jetzt sichtbar: Baum kippt um (easeOut), Totenkopf erscheint nach 1.8 s darüber, nach 3.2 s fadet alles weg. Animation hängt an state.animMs → Pause friert ein. Totenkopf als Canvas-Texture auf einem Billboard-Plane. - simulateTick vergleicht Forest-Count vor/nach engine.tick; orphaned Meshes werden aus placedMeshes gepullt und an fallenTrees übergeben, damit syncMeasureMeshes sie nicht voreilig entfernt. - Toast-Card pro Tick mit Holz-Fäll-Meldung (🪓 'Bewohner:innen fällen N Bäume zum Heizen'), klickbar auf INFO_TOPICS.blackout. - Solaranlage-Kauf: Toast mit Strom-Leistung (+1 MW) und Wartung (2 Mio €/Jahr), analog zum Gründach-Hinweis. Co-Authored-By: Claude Opus 4.7 (1M context) --- App/sims/klima/game-3d.html | 119 ++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) 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); } /* ============================================================