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:
@@ -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);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
Reference in New Issue
Block a user