Klima 3D: Radweg-Snap, Start-Bäume, Krisen-Toasts, Airport-Plateau, Sand-Fixes
- Radwege: snappen an bestehende Pfade an (Distanz 2.2, Spacing 92 %, leicht überlappend). Rotation am NEUEN Mittelpunkt via coastRotation, Kette folgt so der Küstenbiegung statt starre Linie. - Start-Wald: 3 Bäume bei startGame pre-seeded (forest.instances mit starter-Flag). Engine-Blackout-Logik fällt sie bei Stromausfall. Toast 'Du startest mit 3 Bäumen ...' am Level-Start, klickbar auf INFO_TOPICS.blackout. - updateCrisisMusic: Edge-Detection pro Kriterium (Budget/Temp/Flood), jeweils Toast-Card beim Übergang ins Kritische. Merker in startGame zurückgesetzt. - Airport: Plateau-Sockel (3.4×0.20×1.55 + dünne helle Oberkante), alle Bauten um LIFT=0.24 angehoben. Hebt Airport sichtbar aus dem Terrain, kein Versinken am Hang mehr. Heli-Pad-Offset folgt nach. - Sand-Aufschüttung (Bau-Maßnahme): zurück zum reinen Lathe-Kegel (Plattform + Gras raus). Kleiner, zurückhaltender Sand-Hügel wie ursprünglich V1. - Citizen-Event 'Strand aufschütten' (citizen-tourism): neuer syncCitizenBeach → breiter flacher Sand-Streifen vorne am Ufer, parallel zur Küste. Nur sichtbar wenn state.hasSandBeach gesetzt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+202
-72
@@ -1590,9 +1590,33 @@ function simulateTick() {
|
||||
* kritisch wird (Budget negativ ODER Temperatur > 17 °C). Sobald sich die
|
||||
* Lage entspannt, kommt der vorher gehörte Track zurück. Hat der Spieler
|
||||
* selbst aus dem Dropdown etwas ausgewählt, wird NICHT umgeschaltet. */
|
||||
// Merker für Edge-Detection (pro View-Instanz). Reset in startGame.
|
||||
let _wasBudgetCritical = false;
|
||||
let _wasTempCritical = false;
|
||||
let _wasFloodCritical = false;
|
||||
|
||||
function updateCrisisMusic() {
|
||||
if (!window.ggsMusicSetAuto) return;
|
||||
const critical = state.budget < 0 || state.currentTemp > 17 || state.floodedPct > 30;
|
||||
const budgetCrit = state.budget < 0;
|
||||
const tempCrit = state.currentTemp > 17;
|
||||
const floodCrit = state.floodedPct > 30;
|
||||
const critical = budgetCrit || tempCrit || floodCrit;
|
||||
|
||||
// Steigende Flanke Budget → Info-Karte erklärt das Umschalten der Musik
|
||||
// und was jetzt zu tun ist. `infoKey: 'budget'` macht die Karte klickbar.
|
||||
if (budgetCrit && !_wasBudgetCritical) {
|
||||
showEvent('💸', 'Dein Budget ist ins Minus gerutscht — die Stadt macht Schulden. Reiss teure Wartungs-Maßnahmen ab oder baue zunächst nichts Neues. Die Musik wechselt auf Drama, bis du wieder im Plus bist.', 'bad', 'budget');
|
||||
}
|
||||
if (tempCrit && !_wasTempCritical) {
|
||||
showEvent('🌡', 'Du hast +2 °C überschritten — das Pariser Klimaziel ist verfehlt. Viele Folgen sind jetzt unvermeidbar.', 'bad', 'temperature');
|
||||
}
|
||||
if (floodCrit && !_wasFloodCritical) {
|
||||
showEvent('🌊', 'Über 30 % der Stadt sind überflutet — der Durchgang droht zu kippen. Jetzt dringend Küstenschutz aufstocken.', 'bad', 'sealevel');
|
||||
}
|
||||
_wasBudgetCritical = budgetCrit;
|
||||
_wasTempCritical = tempCrit;
|
||||
_wasFloodCritical = floodCrit;
|
||||
|
||||
if (critical) window.ggsMusicSetAuto('drama');
|
||||
else window.ggsMusicRestoreAuto();
|
||||
}
|
||||
@@ -2748,52 +2772,64 @@ function makeCoalPlant() {
|
||||
|
||||
function makeAirport() {
|
||||
const g = new THREE.Group();
|
||||
// Plateau-Sockel: hebt den Flughafen sichtbar aus dem Terrain und
|
||||
// verhindert, dass Teile am Hang im Berg verschwinden. Zwei-Schichten-
|
||||
// Look (Unterkante etwas breiter, Oberkante heller).
|
||||
const plateauBase = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(3.4, 0.20, 1.55),
|
||||
new THREE.MeshStandardMaterial({ color: 0x50555a, roughness: 1 })
|
||||
);
|
||||
plateauBase.position.y = 0.10; g.add(plateauBase);
|
||||
const plateauTop = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(3.3, 0.04, 1.48),
|
||||
new THREE.MeshStandardMaterial({ color: 0x787d82, roughness: 1 })
|
||||
);
|
||||
plateauTop.position.y = 0.22; g.add(plateauTop);
|
||||
// Alle Airport-Strukturen um LIFT anheben (sitzen jetzt auf der Plateau-Kante)
|
||||
const LIFT = 0.24;
|
||||
|
||||
const runway = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2.4, 0.04, 0.45),
|
||||
new THREE.MeshStandardMaterial({ color: 0x404048, roughness: 1 })
|
||||
);
|
||||
runway.position.y = 0.02; g.add(runway);
|
||||
runway.position.y = 0.02 + LIFT; g.add(runway);
|
||||
for (let i = -2; i <= 2; i++) {
|
||||
const stripe = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.18, 0.002, 0.05),
|
||||
new THREE.MeshStandardMaterial({ color: 0xf0f0f0, roughness: 1 })
|
||||
);
|
||||
stripe.position.set(i * 0.4, 0.045, 0); g.add(stripe);
|
||||
stripe.position.set(i * 0.4, 0.045 + LIFT, 0); g.add(stripe);
|
||||
}
|
||||
const terminal = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.8, 0.32, 0.45),
|
||||
new THREE.MeshStandardMaterial({ color: 0xe4e0d4, roughness: 1 })
|
||||
);
|
||||
terminal.position.set(-0.6, 0.18, 0.55); g.add(terminal);
|
||||
terminal.position.set(-0.6, 0.18 + LIFT, 0.55); g.add(terminal);
|
||||
const tower = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.07, 0.08, 0.55, 8),
|
||||
new THREE.MeshStandardMaterial({ color: 0xd0c8b4, roughness: 1 })
|
||||
);
|
||||
tower.position.set(-0.95, 0.43, 0.55); g.add(tower);
|
||||
tower.position.set(-0.95, 0.43 + LIFT, 0.55); g.add(tower);
|
||||
const towerTop = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.18, 0.10, 0.18),
|
||||
new THREE.MeshStandardMaterial({ color: 0x2a4a6a, roughness: 0.6 })
|
||||
);
|
||||
towerTop.position.set(-0.95, 0.78, 0.55); g.add(towerTop);
|
||||
// Heli-Pad: durchgehend schwarze Scheibe mit weißem H-Marking in der
|
||||
// Mitte. Keine farbigen Ränder — klar lesbar als Landeplatz.
|
||||
towerTop.position.set(-0.95, 0.78 + LIFT, 0.55); g.add(towerTop);
|
||||
// Heli-Pad: durchgehend schwarze Scheibe mit weißem H-Marking
|
||||
const padBlack = new THREE.MeshStandardMaterial({ color: 0x000000, roughness: 1 });
|
||||
const padWhite = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 1 });
|
||||
const pad = new THREE.Mesh(new THREE.CylinderGeometry(0.34, 0.34, 0.02, 28), padBlack);
|
||||
pad.position.set(0.85, 0.03, 0.55); g.add(pad);
|
||||
// H aus drei weißen Balken, flach oben auf dem Pad (Y-Achse = Dicke)
|
||||
pad.position.set(0.85, 0.03 + LIFT, 0.55); g.add(pad);
|
||||
const hBarL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.006, 0.28), padWhite);
|
||||
hBarL.position.set(0.73, 0.045, 0.55); g.add(hBarL);
|
||||
hBarL.position.set(0.73, 0.045 + LIFT, 0.55); g.add(hBarL);
|
||||
const hBarR = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.006, 0.28), padWhite);
|
||||
hBarR.position.set(0.97, 0.045, 0.55); g.add(hBarR);
|
||||
hBarR.position.set(0.97, 0.045 + LIFT, 0.55); g.add(hBarR);
|
||||
const hBarM = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.006, 0.06), padWhite);
|
||||
hBarM.position.set(0.85, 0.045, 0.55); g.add(hBarM);
|
||||
// Pad-Welt-Offset relativ zum Airport-Gruppen-Zentrum merken (inkl. 2x-Scale);
|
||||
// der Heli nutzt das als Lande-Position.
|
||||
g.userData.heliPadLocal = { x: 0.85, y: 0.04, z: 0.55 };
|
||||
// Doppelte Größe: Terminal dominiert jetzt klar sichtbar die Insel.
|
||||
hBarM.position.set(0.85, 0.045 + LIFT, 0.55); g.add(hBarM);
|
||||
// Heli-Offset inkl. LIFT, damit die Lande-Position korrekt ist
|
||||
g.userData.heliPadLocal = { x: 0.85, y: 0.04 + LIFT, z: 0.55 };
|
||||
g.scale.setScalar(2);
|
||||
g.userData.isAirport = true; // für Heli-Anker
|
||||
g.userData.isAirport = true;
|
||||
return g;
|
||||
}
|
||||
|
||||
@@ -2882,63 +2918,30 @@ function makeMangrove() {
|
||||
return g;
|
||||
}
|
||||
|
||||
// Sand-Aufschüttung: eine breite, flache Strandplattform (liegt quer zur
|
||||
// Küstenlinie — coastRotation richtet sie automatisch parallel aus) mit
|
||||
// einem kleinen Dünenhügel in der Mitte. Soll klar als künstliche
|
||||
// Strand-Erweiterung lesbar sein: sie ragt sichtbar ins Wasser.
|
||||
// Sand-Aufschüttung: glockenförmiger Sandhügel (Lathe, Cos-Profil). Ist
|
||||
// bewusst klein und zurückhaltend — wirkt als lokaler Dünenabschnitt am
|
||||
// Ufer und nicht als Podest. Parallel-Ausrichtung über coastRotation.
|
||||
function makeSandFill() {
|
||||
const g = new THREE.Group();
|
||||
const sandMat = new THREE.MeshStandardMaterial({ color: 0xf0d890, roughness: 1 });
|
||||
// Hauptplattform: lange Strandbank, 1.8×0.3×0.7 Einheiten
|
||||
const platformShape = new THREE.Shape();
|
||||
platformShape.moveTo(-0.9, -0.30);
|
||||
platformShape.lineTo( 0.9, -0.30);
|
||||
platformShape.lineTo( 1.05, -0.20);
|
||||
platformShape.lineTo( 1.05, 0.20);
|
||||
platformShape.lineTo( 0.9, 0.30);
|
||||
platformShape.lineTo(-0.9, 0.30);
|
||||
platformShape.lineTo(-1.05, 0.20);
|
||||
platformShape.lineTo(-1.05,-0.20);
|
||||
platformShape.closePath();
|
||||
const platformGeo = new THREE.ExtrudeGeometry(platformShape, { depth: 0.28, bevelEnabled: false });
|
||||
platformGeo.rotateX(-Math.PI / 2);
|
||||
const platform = new THREE.Mesh(platformGeo, sandMat);
|
||||
// leichte Höhen-Variationen für Natur-Look
|
||||
const pp = platformGeo.attributes.position;
|
||||
for (let i = 0; i < pp.count; i++) {
|
||||
const x = pp.getX(i), y = pp.getY(i), z = pp.getZ(i);
|
||||
if (y > 0.25) pp.setY(i, y + Math.sin(x * 2.3) * 0.025 + Math.cos(z * 2.1) * 0.02);
|
||||
}
|
||||
platformGeo.computeVertexNormals();
|
||||
g.add(platform);
|
||||
|
||||
// Kleiner Dünenhügel oben auf der Plattform (Lathe → Glockenprofil)
|
||||
const points = [];
|
||||
const segments = 10, radius = 0.55, height = 0.22;
|
||||
const segments = 12, radius = 0.85, height = 0.42;
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const t = i / segments;
|
||||
points.push(new THREE.Vector2(radius * Math.cos(t * Math.PI / 2), t * height));
|
||||
const r = radius * Math.cos(t * Math.PI / 2);
|
||||
points.push(new THREE.Vector2(r, t * height));
|
||||
}
|
||||
const moundGeo = new THREE.LatheGeometry(points, 16);
|
||||
const mp = moundGeo.attributes.position;
|
||||
for (let i = 0; i < mp.count; i++) {
|
||||
const x = mp.getX(i), y = mp.getY(i), z = mp.getZ(i);
|
||||
mp.setY(i, y + Math.sin(x * 5) * 0.012 + Math.cos(z * 4) * 0.01);
|
||||
}
|
||||
moundGeo.computeVertexNormals();
|
||||
const mound = new THREE.Mesh(moundGeo, sandMat);
|
||||
mound.position.y = 0.28; // sitzt auf der Plattform
|
||||
g.add(mound);
|
||||
|
||||
// Ein paar Grashalme/Strandgras-Büschel als Akzent
|
||||
const grassMat = new THREE.MeshStandardMaterial({ color: 0xa8b060, roughness: 1 });
|
||||
for (const [gx, gz] of [[-0.6, 0.15], [0.5, -0.2], [-0.2, 0.22]]) {
|
||||
const grass = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(0.06, 0.18, 5),
|
||||
grassMat
|
||||
);
|
||||
grass.position.set(gx, 0.37, gz); g.add(grass);
|
||||
const geom = new THREE.LatheGeometry(points, 18);
|
||||
const pos = geom.attributes.position;
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);
|
||||
pos.setY(i, y + Math.sin(x * 5) * 0.015 + Math.cos(z * 4) * 0.012);
|
||||
}
|
||||
geom.computeVertexNormals();
|
||||
const sand = new THREE.Mesh(
|
||||
geom,
|
||||
new THREE.MeshStandardMaterial({ color: 0xe6cf8a, roughness: 1 })
|
||||
);
|
||||
g.add(sand);
|
||||
return g;
|
||||
}
|
||||
|
||||
@@ -3061,6 +3064,51 @@ function coastRotation(x, z) {
|
||||
);
|
||||
}
|
||||
|
||||
// Länge eines Radweg-Mesh-Asphalt-Bands (siehe makeBikePath: BoxGeometry 1.4)
|
||||
const BIKE_LENGTH = 1.4;
|
||||
const BIKE_SNAP_RADIUS = 2.2; // ab dieser Nähe zu bestehendem Radweg andocken
|
||||
|
||||
/**
|
||||
* snapBikesPlacement(rawX, rawZ) — wenn ein bestehender Radweg in der
|
||||
* Nähe ist, gibt es die Position zurück, an der das neue Segment
|
||||
* bündig an dessen Stirnseite andockt (gleiche Rotation, Abstand =
|
||||
* BIKE_LENGTH entlang seiner Tangente, vor- oder rückwärts je nachdem
|
||||
* wo der Cursor sitzt). Gibt null zurück, wenn nichts zum Snappen da.
|
||||
*/
|
||||
function snapBikesPlacement(rawX, rawZ) {
|
||||
const bikes = state.ownedMeasures && state.ownedMeasures.bikes;
|
||||
if (!bikes || !bikes.instances || !bikes.instances.length) return null;
|
||||
let best = null, bestDist = Infinity;
|
||||
for (const inst of bikes.instances) {
|
||||
if (!inst.pos) continue;
|
||||
const dx = rawX - inst.pos.x;
|
||||
const dz = rawZ - inst.pos.z;
|
||||
const d = Math.sqrt(dx*dx + dz*dz);
|
||||
if (d < bestDist) { bestDist = d; best = inst; }
|
||||
}
|
||||
if (!best || bestDist > BIKE_SNAP_RADIUS) return null;
|
||||
|
||||
// Lokales +X des bestehenden Mesh in Welt (rotation.y um Y):
|
||||
// (1,0,0) → (cos rot, 0, −sin rot)
|
||||
const rot = best.rotation || 0;
|
||||
const fx = Math.cos(rot);
|
||||
const fz = -Math.sin(rot);
|
||||
// Seite entscheiden: liegt rawPos vor oder hinter dem bestehenden Zentrum?
|
||||
const relX = rawX - best.pos.x;
|
||||
const relZ = rawZ - best.pos.z;
|
||||
const dir = (relX * fx + relZ * fz) >= 0 ? 1 : -1;
|
||||
// Leicht überlappender Abstand (~92 % der Mesh-Länge), damit die Segmente
|
||||
// visuell ineinander greifen statt Lücken zu lassen.
|
||||
const SPACING = BIKE_LENGTH * 0.92;
|
||||
const snapX = best.pos.x + dir * SPACING * fx;
|
||||
const snapZ = best.pos.z + dir * SPACING * fz;
|
||||
const snapY = Math.max(terrainHeight(snapX, snapZ), 0);
|
||||
// Rotation am NEUEN Mittelpunkt aus coastRotation — so folgt die Kette
|
||||
// der Küstenbiegung, Glied für Glied leicht gedreht.
|
||||
const newRot = coastRotation(snapX, snapZ);
|
||||
return { x: snapX, y: snapY, z: snapZ, rotation: newRot };
|
||||
}
|
||||
|
||||
/**
|
||||
* updateZoneOverlay(id) — setzt per-Vertex RGBA-Farben im zoneOverlay
|
||||
* basierend auf isZoneValid. Valide Bereiche werden grün-translucent,
|
||||
@@ -3189,19 +3237,27 @@ function updatePlacementGhost(clientX, clientY) {
|
||||
return;
|
||||
}
|
||||
const hit = hits[0];
|
||||
const x = hit.point.x, z = hit.point.z;
|
||||
let x = hit.point.x, z = hit.point.z;
|
||||
// Höhe immer aus Terrain-Formel lesen, nicht vom Raycast-Hit
|
||||
// (für Maßnahmen auf der Insel, nicht fürs offene Meer).
|
||||
let y;
|
||||
if (zone && zone.kind === 'sea') y = WATER_Y_BASE + 0.1;
|
||||
else y = Math.max(terrainHeight(x, z), 0);
|
||||
placementGhost.position.set(x, y, z);
|
||||
placementPos.x = x; placementPos.y = y; placementPos.z = z;
|
||||
placementValid = isZoneValid(placementId, x, z, terrainHeight(x, z));
|
||||
|
||||
// Coast-Typen: Längsachse parallel zur Küste ausrichten. Andere Maßnahmen
|
||||
// behalten die einmal bei startPlacement gewürfelte Rotation.
|
||||
if (isCoastMeasure(placementId)) placementRotation = coastRotation(x, z);
|
||||
|
||||
// Bike-Snap: wenn ein bestehender Radweg in Reichweite liegt, dock das
|
||||
// Ghost an dessen Stirnseite an (wie Schienen). Länge = BIKE_LENGTH.
|
||||
if (placementId === 'bikes') {
|
||||
const snap = snapBikesPlacement(x, z);
|
||||
if (snap) { x = snap.x; z = snap.z; y = snap.y; placementRotation = snap.rotation; }
|
||||
}
|
||||
|
||||
placementGhost.position.set(x, y, z);
|
||||
placementPos.x = x; placementPos.y = y; placementPos.z = z;
|
||||
placementValid = isZoneValid(placementId, x, z, terrainHeight(x, z));
|
||||
placementGhost.rotation.y = placementRotation;
|
||||
|
||||
// Ghost-Farbe: grün = valide, rot = nicht valide (via emissive)
|
||||
@@ -3940,6 +3996,55 @@ function syncMountainShield() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bürger-Event „Strand künstlich aufschütten" (citizen-tourism) ---
|
||||
// Setzt state.hasSandBeach = true. Wir zeigen dann vorne an der Insel eine
|
||||
// angehobene Strand-Schwelle als breiten, flachen Sand-Streifen parallel
|
||||
// zur Küste. Das ist kein einzelnes Mesh pro Kauf, sondern eine einmalige
|
||||
// Stadt-Investition (daher separat vom makeSandFill der Bau-Maßnahme).
|
||||
let citizenBeach = null;
|
||||
function buildCitizenBeach() {
|
||||
const g = new THREE.Group();
|
||||
g.userData.infoKey = 'sand_fill_info';
|
||||
const sandMat = new THREE.MeshStandardMaterial({ color: 0xf0d890, roughness: 1 });
|
||||
// Flacher, länglicher Strandwall vorne am Ufer
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(-3.5, -0.45);
|
||||
shape.lineTo( 3.5, -0.45);
|
||||
shape.lineTo( 3.9, -0.25);
|
||||
shape.lineTo( 3.9, 0.25);
|
||||
shape.lineTo( 3.5, 0.45);
|
||||
shape.lineTo(-3.5, 0.45);
|
||||
shape.lineTo(-3.9, 0.25);
|
||||
shape.lineTo(-3.9, -0.25);
|
||||
shape.closePath();
|
||||
const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.28, bevelEnabled: false });
|
||||
geo.rotateX(-Math.PI / 2);
|
||||
// Leichte sin-Wellen oben auf dem Deckel
|
||||
const pp = geo.attributes.position;
|
||||
for (let i = 0; i < pp.count; i++) {
|
||||
const x = pp.getX(i), y = pp.getY(i), z = pp.getZ(i);
|
||||
if (y > 0.26) pp.setY(i, y + Math.sin(x * 1.3) * 0.02 + Math.cos(z * 2.0) * 0.015);
|
||||
}
|
||||
geo.computeVertexNormals();
|
||||
const strand = new THREE.Mesh(geo, sandMat);
|
||||
// An den vorderen Küstenrand pflanzen, parallel zur Küste dort (+Z-Seite)
|
||||
strand.position.set(0, Math.max(terrainHeight(0, ISLAND_HALF_LENGTH * 0.86), 0), ISLAND_HALF_LENGTH * 0.86);
|
||||
// Tangente an Ellipse vorn (x=0, z=+ISLAND_HALF_LENGTH*0.86) ist entlang ±X
|
||||
strand.rotation.y = coastRotation(0, ISLAND_HALF_LENGTH * 0.86);
|
||||
g.add(strand);
|
||||
return g;
|
||||
}
|
||||
function syncCitizenBeach() {
|
||||
const want = !!state.hasSandBeach;
|
||||
if (want && !citizenBeach) {
|
||||
citizenBeach = buildCitizenBeach();
|
||||
scene.add(citizenBeach);
|
||||
} else if (!want && citizenBeach) {
|
||||
scene.remove(citizenBeach);
|
||||
citizenBeach = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Zentrale Ambient-Update-Funktion (aus sceneRender pro Frame)
|
||||
function updateAmbient(tNow, dt) {
|
||||
updateBoats(tNow);
|
||||
@@ -4078,6 +4183,24 @@ function startGame(levelId) {
|
||||
tutorialStep: 0, tutorialActive: false,
|
||||
pendingCitizenEvent: null,
|
||||
});
|
||||
_wasBudgetCritical = false;
|
||||
_wasTempCritical = false;
|
||||
_wasFloodCritical = false;
|
||||
|
||||
// --- Start-Wald: drei Bäume als Anfangsbestand. Bei Stromausfall
|
||||
// fällt die Engine pro Jahr `treesPerBlackout` Bäume (engine.js L611+),
|
||||
// popt sie aus instances und reduziert count — die Meshes
|
||||
// verschwinden entsprechend aus der Szene (syncMeasureMeshes).
|
||||
// Die drei Start-Bäume sind nicht im actionLog (kein Player-Kauf),
|
||||
// zählen aber beim `forester`-Achievement mit.
|
||||
const forestSeed = { count: 0, instances: [] };
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const meta = pickBuildPosition('forest');
|
||||
forestSeed.instances.push({ builtAt: 0, starter: true, ...meta });
|
||||
forestSeed.count++;
|
||||
}
|
||||
state.ownedMeasures.forest = forestSeed;
|
||||
KlimaEngine.recalcEffects(state); // co2Reduction etc. mit Seed-Wald neu aufsummieren
|
||||
|
||||
// --- 2. UI-Caches ---
|
||||
_prevAchHash = '';
|
||||
@@ -4114,8 +4237,15 @@ function startGame(levelId) {
|
||||
// hasMountainShield in createGame nicht gesetzt → false/undefined).
|
||||
buildVillage();
|
||||
syncMountainShield();
|
||||
syncCitizenBeach();
|
||||
syncMeasureMeshes(); // die drei Start-Bäume sichtbar machen
|
||||
renderUI();
|
||||
startTutorial();
|
||||
// Hinweis-Card: erklärt die Start-Bäume + Blackout-Mechanik. infoKey
|
||||
// 'blackout' macht sie klickbar → INFO_TOPICS.blackout-Detailseite.
|
||||
setTimeout(() => {
|
||||
showEvent('🌲', 'Du startest mit 3 Bäumen. Bei Stromausfall werden sie als Brennholz gefällt — baue rechtzeitig Wind oder Solar.', 'neutral', 'blackout');
|
||||
}, 900);
|
||||
}
|
||||
|
||||
function endLevel(won, reason) {
|
||||
|
||||
Reference in New Issue
Block a user