Klima 3D: Strandleben tourismus-reaktiv + Radweg-Dashes durchgehend

- beachLifeCount(): base 8 + 14 wenn state.tourismMode + 4 pro Airport,
  gedeckelt bei 40. Skaliert die Anzahl der Strandleben-Elemente an
  die Tourismus-Aktivität (mehr Tourismus = mehr Menschen am Strand).
  syncBeachLife() wird pro Frame gerufen, rebuildt aber nur, wenn sich
  die Zahl wirklich geändert hat (O(1) Check bei Stabilität).
- Radwege: zurück zu gestrichelter Mittellinie (5 Striche × Periode
  0.28). BIKE_SPACING auf exakt BIKE_LENGTH (1.4) gesetzt (vorher
  0.92·BIKE_LENGTH), damit die Dashes nahtlos über Segment-Grenzen
  weiterlaufen — Strich des nächsten Segments landet genau dort, wo
  die Dash-Periode des vorherigen weitergeht.
- Ausrichtung bleibt via coastRotation: Radwege folgen weiter der
  gleichen Ellipsen-Tangente wie das grüne Zone-Overlay beim
  Platzieren, sind also parallel zur Küstenkurve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 14:53:20 +02:00
parent ce0164264f
commit af9ecf6b22
+49 -17
View File
@@ -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