Klima 3D: Baumfäll-Animation + Solar-Kauf-Hinweis

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 14:01:13 +02:00
parent f0fd54f458
commit c7b3636815
+119
View File
@@ -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);
}
/* ============================================================