diff --git a/App/sims/klima/game-3d.html b/App/sims/klima/game-3d.html index b5f1c7a..20fd061 100644 --- a/App/sims/klima/game-3d.html +++ b/App/sims/klima/game-3d.html @@ -2953,21 +2953,24 @@ function makeCloudSeeder() { function makeBikePath() { const g = new THREE.Group(); - // Asphalt bleibt als Grundplatte + // Asphalt 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); - // 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 + // Gestrichelte Mittellinie: 5 gleichlange Striche mit Periode 0.28 + // (Strich 0.18, Lücke 0.10). Wird zusammen mit BIKE_SPACING = 1.4 + // (= Bike-Länge) ausgelegt, damit das Muster nahtlos über Segment- + // Grenzen weiterläuft, wenn mehrere Radwege aneinander gesnappt + // werden. + const stripeMat = new THREE.MeshStandardMaterial({ color: 0xf8f8f8, roughness: 1 }); + for (let i = -2; i <= 2; i++) { + const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.006, 0.05), stripeMat); + stripe.position.set(i * 0.28, 0.045, 0); + g.add(stripe); + } + // Zwei Rad-Pictogramm-Stelen am Rand for (const sx of [-0.55, 0.55]) { const post = new THREE.Mesh( new THREE.CylinderGeometry(0.02, 0.025, 0.22, 5), @@ -3209,9 +3212,11 @@ function snapBikesPlacement(rawX, rawZ) { 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; + // Exakte Bike-Länge als Spacing: Dashes der Mittellinie (Periode 0.28, + // 5 Striche pro Segment) laufen dadurch nahtlos über Segment-Grenzen + // — ein Strich des nächsten Segments liegt genau dort, wo die + // Dash-Periode des vorherigen weitergehen würde. + const SPACING = BIKE_LENGTH; const snapX = best.pos.x + dir * SPACING * fx; const snapZ = best.pos.z + dir * SPACING * fz; const snapY = Math.max(terrainHeight(snapX, snapZ), 0); @@ -4294,16 +4299,37 @@ function makeBeachDog() { } return g; } +// Dynamische Anzahl Strandleben abhängig von Tourismus-Aktivität: +// Base 8 (Einheimische) + 14 wenn tourismMode aktiv + 4 pro Flughafen. +// Gedeckelt bei 40, damit die Szene nicht platzt. +function beachLifeCount() { + const base = 8; + const tourismBonus = state && state.tourismMode ? 14 : 0; + const airports = (state && state.ownedMeasures && state.ownedMeasures.airport + && state.ownedMeasures.airport.count) || 0; + return Math.min(40, base + tourismBonus + airports * 4); +} +let _lastBeachCount = -1; +function syncBeachLife() { + const c = beachLifeCount(); + if (c !== _lastBeachCount) { + _lastBeachCount = c; + buildBeachLife(); + } +} + function buildBeachLife() { while (beachLifeGroup.children.length) beachLifeGroup.remove(beachLifeGroup.children[0]); - // 22 Stellen rund um die Küste, gleichmäßig verteilt — Figuren, - // Schirme, Handtücher, Liegestühle und Hunde in wechselnder Mischung. - for (let i = 0; i < 22; i++) { + // Anzahl dynamisch: mehr Tourismus = mehr Strandleben (bis 40). + // Gleichmäßig auf Winkel-Raster mit Jitter verteilt, damit die Elemente + // rundum verteilt sitzen statt an einer Stelle zu klumpen. + const count = beachLifeCount(); + for (let i = 0; i < count; i++) { let x = 0, z = 0, y = 0; for (let tries = 0; tries < 24; tries++) { // Winkel aus gleichmäßigem Raster (mit Jitter), damit nicht alle // Elemente zufällig an derselben Seite der Insel hocken. - const angleBase = (i / 22) * Math.PI * 2; + const angleBase = (i / count) * Math.PI * 2; const a = angleBase + (Math.random() - 0.5) * 0.5; const rScale = 0.80 + Math.random() * 0.10; x = Math.cos(a) * ISLAND_HALF_WIDTH * rScale; @@ -4694,6 +4720,10 @@ function updateAmbient(tNow, dt) { updateVillage(tNow, dt); updateClouds(tNow); updateFallenTrees(dt); + // Strandleben-Anzahl reaktiv an Tourismus-Zustand + Airport-Anzahl. + // syncBeachLife ist billig (nur Count-Vergleich), rebuildt nur bei + // Änderung. + syncBeachLife(); } /* ============================================================ @@ -4915,7 +4945,9 @@ function startGame(levelId) { // Start-Dorf aufstellen + Vulkan-Schutzwall zurücksetzen (Engine hatte // hasMountainShield in createGame nicht gesetzt → false/undefined). buildVillage(); + _lastBeachCount = -1; // Strandleben-Zahl beim nächsten sync neu auswerten buildBeachLife(); + _lastBeachCount = beachLifeCount(); syncMountainShield(); syncCitizenBeach(); syncMeasureMeshes(); // die drei Start-Bäume sichtbar machen