Klima 3D: Wolken, kleineres Airport-Plateau, Pause-Zeichen zentral

- Wolken am Himmel: 4 Dunst-Gruppen (je 3-5 Kugeln, transluzent), ziehen
  vom +X Rand über die Insel Richtung Vulkan (−X, −Z-Drift). Eigene
  Phase, Höhe, Zyklusdauer pro Wolke. updateClouds in updateAmbient.
- Airport-Plateau: von 3.4×1.55 auf 2.55×1.25 geschrumpft, nach +Z
  versetzt (0.28). Endet knapp innerhalb der Runway, deckt aber
  Terminal + Heli-Pad weiter ab.
- Pause-Zentrum: neue Card mit ▶-Symbol in der Mitte der Szene, sichtbar
  bei state.speed === 0. Klick startet 1×-Speed. Glas-Optik, animation
  bei Einblendung. Sync via updatePauseCenter in renderUI und bei jedem
  Speed-Klick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 13:58:22 +02:00
parent 9a8ce64d09
commit f0fd54f458
+129 -6
View File
@@ -161,6 +161,52 @@
.kw-place-hint.visible { opacity: 1; }
.kw-place-hint.invalid { background: rgba(176,74,58,0.92); }
/* Zentrales Pause-Zeichen über der Szene — erscheint bei Speed 0 */
.kw-pause-center {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
pointer-events: auto;
cursor: pointer;
user-select: none;
text-align: center;
animation: kw-pause-fade-in 0.2s ease-out;
}
.kw-pause-btn {
width: 110px; height: 110px;
border-radius: 50%;
background: rgba(30, 40, 50, 0.58);
backdrop-filter: blur(10px) saturate(140%);
-webkit-backdrop-filter: blur(10px) saturate(140%);
color: #fff;
font-size: 56px;
line-height: 110px;
border: 3px solid rgba(255,255,255,0.78);
box-shadow: 0 6px 20px rgba(0,0,0,0.28);
transition: transform 0.15s var(--ggs-ease), background 0.18s;
text-indent: 8px; /* optische Mitte für die ▶-Spitze */
}
.kw-pause-center:hover .kw-pause-btn {
transform: scale(1.08);
background: rgba(74, 124, 138, 0.75);
}
.kw-pause-label {
margin-top: 10px;
display: inline-block;
background: rgba(30, 40, 50, 0.72);
color: #fff;
padding: 5px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
@keyframes kw-pause-fade-in {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.85); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
/* „Bau beenden"-Karte — kompakt, kontrastfarben, nur während Placement */
.kw-exit-card {
display: flex;
@@ -579,6 +625,10 @@
von Seitenrand zu Seitenrand — Panels schweben als Glasflächen drüber) -->
<div id="scene-container">
<div class="kw-place-hint" id="place-hint">Klicke zum Platzieren</div>
<div class="kw-pause-center" id="pause-center" style="display:none" title="Fortsetzen">
<div class="kw-pause-btn"></div>
<div class="kw-pause-label">Pause — klicken zum Fortsetzen</div>
</div>
</div>
<!-- LINKS: Status ------------------------------------------------ -->
@@ -2058,6 +2108,7 @@ function renderUI() {
renderGraphs();
renderAchievements();
renderTimeline();
if (typeof updatePauseCenter === 'function') updatePauseCenter();
// Szene wird aus der Frame-Loop gezeichnet (sceneRender), hier nicht
// nötig — anders als im 2D-Canvas-Renderer, der jedes State-Update
// synchron neu zeichnen musste.
@@ -2781,18 +2832,19 @@ function makeCoalPlant() {
function makeAirport() {
const g = new THREE.Group();
// Plateau-Sockel: hebt den Flughafen sichtbar aus dem Terrain und
// verhindert, dass Teile am Hang im Berg verschwinden. Zwei-Schichten-
// Look (Unterkante etwas breiter, Oberkante heller).
// verhindert, dass Teile am Hang im Berg verschwinden. Länge bleibt
// knapp innerhalb der Runway (2.4), Z-Richtung ist nach +Z versetzt,
// um Terminal + Heli-Pad abzudecken (sitzen beide bei z≈0.55).
const plateauBase = new THREE.Mesh(
new THREE.BoxGeometry(3.4, 0.20, 1.55),
new THREE.BoxGeometry(2.55, 0.20, 1.25),
new THREE.MeshStandardMaterial({ color: 0x50555a, roughness: 1 })
);
plateauBase.position.y = 0.10; g.add(plateauBase);
plateauBase.position.set(0, 0.10, 0.28); g.add(plateauBase);
const plateauTop = new THREE.Mesh(
new THREE.BoxGeometry(3.3, 0.04, 1.48),
new THREE.BoxGeometry(2.48, 0.04, 1.18),
new THREE.MeshStandardMaterial({ color: 0x787d82, roughness: 1 })
);
plateauTop.position.y = 0.22; g.add(plateauTop);
plateauTop.position.set(0, 0.22, 0.28); g.add(plateauTop);
// Alle Airport-Strukturen um LIFT anheben (sitzen jetzt auf der Plateau-Kante)
const LIFT = 0.24;
@@ -4053,11 +4105,65 @@ function syncCitizenBeach() {
}
}
// --- Wolken am Himmel ---
// Ziehen als Dunst-Gruppen quer über die Insel, eher Richtung Vulkan
// (z-Drift nach hinten). Low-Poly-Sphären, transluzent, ohne Schatten —
// pro Wolke eigener Zyklus und eigenes Höhenlevel.
function makeCloud() {
const g = new THREE.Group();
const mat = new THREE.MeshBasicMaterial({
color: 0xffffff, transparent: true, opacity: 0.72, depthWrite: false
});
// Drei bis fünf gestapelte Kugeln für Dunst-Silhouette
const bumps = 3 + Math.floor(Math.random() * 3);
for (let i = 0; i < bumps; i++) {
const r = 0.9 + Math.random() * 0.9;
const sphere = new THREE.Mesh(new THREE.SphereGeometry(r, 8, 6), mat);
sphere.position.set(
(i - bumps / 2) * 1.1 + (Math.random() - 0.5) * 0.4,
(Math.random() - 0.5) * 0.4,
(Math.random() - 0.5) * 0.4
);
g.add(sphere);
}
return g;
}
// Vier Wolken, jede mit eigener Phase, Höhe und Flugdauer.
const CLOUD_CYCLE = 60; // Sekunden pro Überquerung (Basis)
const clouds = [];
for (let i = 0; i < 4; i++) {
const c = makeCloud();
c.userData.phase = Math.random(); // 0..1 Startverschiebung
c.userData.height = 13 + Math.random() * 4; // y
c.userData.zStart = 4 + Math.random() * 6; // Insel-Vorder-Bereich
c.userData.zEnd = -16 - Math.random() * 4; // hinterm Vulkan
c.userData.scale = 0.8 + Math.random() * 0.5;
c.userData.speed = 0.8 + Math.random() * 0.6;
c.scale.setScalar(c.userData.scale);
ambientGroup.add(c);
clouds.push(c);
}
function updateClouds(tNow) {
for (const c of clouds) {
const cycle = CLOUD_CYCLE / c.userData.speed;
const u = ((tNow / cycle) + c.userData.phase) % 1; // 0..1 entlang der Bahn
// Von +X nach -X, Z interpoliert von zStart zu zEnd (Drift Richtung Vulkan)
c.position.set(
50 - u * 100,
c.userData.height,
c.userData.zStart + (c.userData.zEnd - c.userData.zStart) * u
);
}
}
// Zentrale Ambient-Update-Funktion (aus sceneRender pro Frame)
function updateAmbient(tNow, dt) {
updateBoats(tNow);
updateHeli(tNow);
updateVillage(tNow, dt);
updateClouds(tNow);
}
/* ============================================================
@@ -4108,9 +4214,26 @@ $$('.ggs-speed-btn').forEach(btn => {
const header = $('#header');
if (sp === 0) { state.phase = state.phase === 'playing' ? 'paused' : state.phase; header.classList.add('paused'); }
else { if (state.phase === 'paused') state.phase = 'playing'; header.classList.remove('paused'); state.lastTickMs = 0; }
updatePauseCenter();
});
});
// Pause-Zeichen zentral in der Szene: sichtbar bei speed=0, sonst aus.
// Klick entspricht dem Druck auf den 1×-Speed-Button.
const pauseCenter = $('#pause-center');
function updatePauseCenter() {
if (!pauseCenter) return;
const visible = state && state.speed === 0
&& (state.phase === 'playing' || state.phase === 'paused' || state.phase === 'tutorial');
pauseCenter.style.display = visible ? '' : 'none';
}
if (pauseCenter) {
pauseCenter.addEventListener('click', () => {
const btn = document.querySelector('[data-speed="1"]');
if (btn) btn.click();
});
}
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'p' || e.key === 'P') document.querySelector('[data-speed="0"]').click();