From f0fd54f458100c005357c93688d12397a0e59cf1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 19 Apr 2026 13:58:22 +0200 Subject: [PATCH] Klima 3D: Wolken, kleineres Airport-Plateau, Pause-Zeichen zentral MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- App/sims/klima/game-3d.html | 135 ++++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 6 deletions(-) diff --git a/App/sims/klima/game-3d.html b/App/sims/klima/game-3d.html index 2fd2287..8abc903 100644 --- a/App/sims/klima/game-3d.html +++ b/App/sims/klima/game-3d.html @@ -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) -->
Klicke zum Platzieren
+
@@ -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();