diff --git a/App/sims/klima/game-3d.html b/App/sims/klima/game-3d.html index f7b63f8..f8e635e 100644 --- a/App/sims/klima/game-3d.html +++ b/App/sims/klima/game-3d.html @@ -2956,18 +2956,21 @@ function makeCloudSeeder() { function makeBikePath() { const g = new THREE.Group(); + // Asphalt bleibt als Grundplatte const asphalt = new THREE.Mesh( new THREE.BoxGeometry(1.4, 0.04, 0.45), new THREE.MeshStandardMaterial({ color: 0x3a3a3a, roughness: 1 }) ); asphalt.position.y = 0.02; g.add(asphalt); - for (let i = -2; i <= 2; i++) { - const stripe = new THREE.Mesh( - new THREE.BoxGeometry(0.18, 0.005, 0.04), - new THREE.MeshStandardMaterial({ color: 0xf2f2f2, roughness: 1 }) - ); - stripe.position.set(i * 0.28, 0.045, 0); g.add(stripe); - } + // Durchgehende weiße Mittellinie: eine einzige Box über die volle + // Asphalt-Länge. Bei snap-spacing 1.288 (Überlappung 8 %) greifen + // benachbarte Segmente ineinander → visuell eine durchlaufende Linie. + const centerline = new THREE.Mesh( + new THREE.BoxGeometry(1.4, 0.006, 0.05), + new THREE.MeshStandardMaterial({ color: 0xf8f8f8, roughness: 1 }) + ); + centerline.position.y = 0.045; g.add(centerline); + // Zwei Rad-Pictogramm-Stelen am Rand für Deko for (const sx of [-0.55, 0.55]) { const post = new THREE.Mesh( new THREE.CylinderGeometry(0.02, 0.025, 0.22, 5), @@ -3114,6 +3117,14 @@ function syncMeasureMeshes() { const p = inst.pos || { x: 0, y: 0, z: 0 }; mesh.position.set(p.x, p.y, p.z); if (typeof inst.rotation === 'number') mesh.rotation.y = inst.rotation; + // Airport: Runway-Ausrichtung IMMER vom Vulkan weg erzwingen + // (greift auch bei alten Saves mit zufälliger Rotation). + if (id === 'airport') { + const dx = p.x - VOLCANO_X; + const dz = p.z - VOLCANO_Z; + mesh.rotation.y = Math.atan2(-dz, dx); + inst.rotation = mesh.rotation.y; + } measuresGroup.add(mesh); placedMeshes.set(key, mesh); }); @@ -4171,6 +4182,76 @@ const villageHouses = []; // stabile Liste, nur Visibility toggelt mit Populat const smokePuffs = []; // [{mesh, age, lifetime, vy}] const smokeGeom = new THREE.SphereGeometry(0.08, 6, 5); +// --- Strandleben: winzige Figuren + Sonnenschirme am Strand --- +// Rein dekorativ, wird einmal beim Level-Start aufgebaut und bleibt +// stehen. Spawnt auf dem Küstenband (Terrain-Höhe 0.25..0.45). +const beachLifeGroup = new THREE.Group(); +scene.add(beachLifeGroup); +function makeBeachFigure() { + const g = new THREE.Group(); + const colors = [0xd04040, 0x2a60a0, 0xe8c080, 0x60a050, 0xc0a040]; + const body = new THREE.Mesh( + new THREE.BoxGeometry(0.06, 0.14, 0.04), + new THREE.MeshStandardMaterial({ color: colors[Math.floor(Math.random() * colors.length)], roughness: 1 }) + ); + body.position.y = 0.07; g.add(body); + const head = new THREE.Mesh( + new THREE.SphereGeometry(0.03, 6, 4), + new THREE.MeshStandardMaterial({ color: 0xe8c8a0, roughness: 1 }) + ); + head.position.y = 0.17; g.add(head); + return g; +} +function makeBeachUmbrella() { + const g = new THREE.Group(); + const pole = new THREE.Mesh( + new THREE.CylinderGeometry(0.006, 0.006, 0.18, 6), + new THREE.MeshStandardMaterial({ color: 0x8a6040, roughness: 1 }) + ); + pole.position.y = 0.09; g.add(pole); + const colors = [0xd04040, 0xe8a040, 0x4a8aa0, 0xc060a0]; + const shade = new THREE.Mesh( + new THREE.ConeGeometry(0.10, 0.05, 8), + new THREE.MeshStandardMaterial({ color: colors[Math.floor(Math.random() * colors.length)], roughness: 1 }) + ); + shade.position.y = 0.21; g.add(shade); + return g; +} +function makeBeachTowel() { + const g = new THREE.Group(); + const colors = [0xd04040, 0xf0d040, 0x4a8aa0, 0xc060a0, 0x40c080]; + const towel = new THREE.Mesh( + new THREE.BoxGeometry(0.14, 0.003, 0.08), + new THREE.MeshStandardMaterial({ color: colors[Math.floor(Math.random() * colors.length)], roughness: 1 }) + ); + towel.position.y = 0.002; g.add(towel); + return g; +} +function buildBeachLife() { + while (beachLifeGroup.children.length) beachLifeGroup.remove(beachLifeGroup.children[0]); + // 12 Stellen am Küstenband, zufällig auf Figuren/Schirme/Handtücher verteilt + for (let i = 0; i < 12; i++) { + let x = 0, z = 0, y = 0; + for (let tries = 0; tries < 20; tries++) { + const a = Math.random() * Math.PI * 2; + const rScale = 0.82 + Math.random() * 0.08; + x = Math.cos(a) * ISLAND_HALF_WIDTH * rScale; + z = Math.sin(a) * ISLAND_HALF_LENGTH * rScale; + y = terrainHeight(x, z); + if (y >= 0.2 && y <= 0.55) break; + } + if (y < 0.2) continue; + const roll = Math.random(); + const piece = roll < 0.45 ? makeBeachFigure() + : roll < 0.80 ? makeBeachUmbrella() + : makeBeachTowel(); + piece.position.set(x, y, z); + piece.rotation.y = Math.random() * Math.PI * 2; + piece.scale.setScalar(0.55 + Math.random() * 0.35); // winzig + beachLifeGroup.add(piece); + } +} + function buildVillage() { // Alte Häuser + Puffs raus for (const h of villageHouses) villageGroup.remove(h); @@ -4760,6 +4841,7 @@ function startGame(levelId) { // Start-Dorf aufstellen + Vulkan-Schutzwall zurücksetzen (Engine hatte // hasMountainShield in createGame nicht gesetzt → false/undefined). buildVillage(); + buildBeachLife(); syncMountainShield(); syncCitizenBeach(); syncMeasureMeshes(); // die drei Start-Bäume sichtbar machen @@ -5064,6 +5146,7 @@ function deserializeState(d) { } syncMeasureMeshes(); buildVillage(); // Dorf passend zur geladenen Bevölkerung + buildBeachLife(); // Strandleben beim Laden ebenfalls neu setzen syncMountainShield(); // Schutzwall-Flag aus Save übernehmen const badge = $('#level-badge'); if (badge) badge.textContent = 'Level ' + state.difficulty + ' · ' + state.diff.label;