Klima 3D: Strandleben winzig + Radweg-Mittellinie durchgehend + Airport-Sync

- Strandleben (beachLifeGroup): 12 winzige Figuren / Sonnenschirme /
  Strandhandtücher am Küstenband (Terrain 0.20..0.55), Scale 0.55..0.90.
  makeBeachFigure / makeBeachUmbrella / makeBeachTowel; buildBeachLife
  wird von startGame und deserializeState gerufen.
- Radwege: 5 einzelne Stripes durch EINE durchgehende weiße Mittellinie
  ersetzt (Box 1.4×0.006×0.05). Bei snap-Spacing 0.92 × BIKE_LENGTH
  überlappen sich die Mittellinien benachbarter Segmente zu einer
  visuell kontinuierlichen Linie entlang der Küstenbiegung.
- syncMeasureMeshes erzwingt für Airport-Meshes jetzt IMMER die
  away-from-volcano-Rotation (auch für alte Saves, die noch mit
  zufälliger Rotation gespeichert wurden). inst.rotation wird dabei
  korrigiert, damit spätere Saves konsistent sind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 14:33:09 +02:00
parent e221f14f5c
commit b77a453306
+90 -7
View File
@@ -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;