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;