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