Files
geograsim/App/regenwald.html
T
Adminator f22c5ebbfe Stand 2026-04-13: PHP/MySQL Infrastruktur, Flussmanagement, Stadt-Prototyp
- PHP/MySQL Backend (XAMPP + Produktionsserver)
- Front-Controller, API-Endpunkte, Session-Management
- Flussmanagement-Simulation (Echtzeit, Punkt-basierter Fluss)
- Stadt & Raumplanung (Prototyp, Top-Down Kachelsystem)
- Klimawaechter 3D: Deiche kleiner, Baeume kippen, Budget angepasst
- persistence.ts: Dualer Speicher (localStorage + Server-API)
- 6 Unit-Test-Dateien fuer bestehende Simulationen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:43:42 +02:00

519 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Forscher*in im Regenwald</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
:root {
--text: #1a1a1a; --text2: #4a4a4a; --text3: #8a8a8a;
--fjord: #4a7c8a; --fjord-l: #dae8ec; --fjord-d: #3a6470;
--moss: #5a8a5e; --moss-l: #dceadd;
--sand: #c4a35a; --sand-l: #f2eacc;
}
html, body { height:100%; overflow:hidden; }
body { font-family:'Inter',system-ui,sans-serif; color:var(--text); }
/* === FLUSS-KARTE (scrollender Hintergrund) === */
#river-scene {
position: fixed; inset: 0; z-index: 0;
overflow: hidden;
/* Der Gradient wandert von unten (Delta/warm) nach oben (Berg/kühl) */
background: linear-gradient(0deg, #a8d8c8 0%, #5a9a6a 40%, #3a7a4a 70%, #2a5a3a 100%);
}
/* Flussbett — ein vertikaler Streifen in der Mitte */
#river-bed {
position: absolute;
left: 50%; transform: translateX(-50%);
width: 140px;
top: 0; bottom: 0;
background: linear-gradient(180deg, rgba(60,120,160,.5) 0%, rgba(40,90,130,.65) 100%);
border-left: 2px solid rgba(40,80,120,.3);
border-right: 2px solid rgba(40,80,120,.3);
border-radius: 10px;
}
/* Wellen-Effekt (CSS, keine Sprites) */
#river-bed::before {
content: '';
position: absolute; inset: 0;
background: repeating-linear-gradient(
0deg,
transparent 0px,
transparent 28px,
rgba(255,255,255,.12) 30px,
transparent 32px
);
animation: wave 3s linear infinite;
}
@keyframes wave { from { background-position-y: 0; } to { background-position-y: 60px; } }
/* === UFER-BÄUME (einfache Emoji-Reihen links und rechts) === */
.tree-row {
position: absolute; top: 0; bottom: 0;
width: 80px;
display: flex; flex-direction: column; gap: 35px;
padding-top: 20px;
font-size: 1.6rem; line-height: 1;
overflow: hidden;
pointer-events: none;
}
.tree-row.left { left: calc(50% - 140px); text-align: right; }
.tree-row.right { right: calc(50% - 140px); text-align: left; }
/* === BOOT === */
#boat {
position: fixed;
left: 50%; bottom: 38%;
transform: translateX(-50%);
font-size: 2.6rem;
z-index: 10;
transition: bottom .8s cubic-bezier(.22,1,.36,1);
filter: drop-shadow(0 4px 8px rgba(0,0,0,.25));
pointer-events: none;
}
/* === ENTDECKUNGS-MARKER === */
.discovery-marker {
position: absolute;
left: 50%;
transform: translateX(-50%);
z-index: 8;
cursor: pointer;
transition: all .25s cubic-bezier(.22,1,.36,1);
}
.dm-bubble {
display: flex; align-items: center; justify-content: center;
width: 52px; height: 52px;
border-radius: 50%;
background: rgba(255,255,255,.85);
box-shadow: 0 4px 16px rgba(0,40,30,.22);
border: 2.5px solid rgba(255,255,255,.8);
font-size: 1.7rem;
transition: all .2s;
}
.dm-bubble:hover { transform: scale(1.2); box-shadow: 0 6px 24px rgba(0,40,30,.35); }
.dm-bubble.found { border-color: var(--moss); background: var(--moss-l); }
.dm-bubble.mystery {
background: rgba(255,255,255,.5);
border: 2.5px dashed rgba(0,0,0,.2);
font-size: 1.4rem; color: var(--text3);
}
.dm-name {
text-align: center;
font-size: .62rem; font-weight: 700;
color: #fff;
text-shadow: 0 1px 4px rgba(0,0,0,.5);
margin-top: .2rem;
white-space: nowrap;
}
/* === INFO-KARTE (erscheint bei Klick auf ein Tier) === */
.info-overlay {
position: fixed; inset: 0; z-index: 50;
background: rgba(15,35,25,.75); backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center;
padding: 1.5rem;
animation: fadein .25s ease-out;
}
@keyframes fadein { from { opacity:0; } to { opacity:1; } }
.info-card {
background: #fff; border-radius: 20px;
padding: 1.8rem 2rem; max-width: 480px; width: 100%;
box-shadow: 0 30px 80px rgba(0,0,0,.4);
animation: cardpop .3s cubic-bezier(.22,1,.36,1);
}
@keyframes cardpop {
from { transform: scale(.9) translateY(15px); opacity:0; }
to { transform: scale(1) translateY(0); opacity:1; }
}
.info-card .ic-emoji { font-size: 3.5rem; text-align: center; margin-bottom: .5rem; }
.info-card h3 { font-size: 1.3rem; font-weight: 800; color: var(--fjord); margin-bottom: .5rem; text-align: center; }
.info-card .ic-zone {
text-align: center; font-size: .62rem; font-weight: 700;
color: var(--moss); text-transform: uppercase; letter-spacing: .06em;
margin-bottom: .8rem;
}
.info-card p { font-size: .95rem; color: var(--text2); line-height: 1.6; margin-bottom: .7rem; }
.info-card .ic-wow {
background: var(--sand-l); border-left: 3px solid var(--sand);
padding: .7rem .9rem; border-radius: 0 8px 8px 0;
font-size: .85rem; color: var(--text2); margin-bottom: .9rem;
}
.info-card .ic-wow::before { content: '🤯 '; }
.info-card .ic-btn {
width: 100%; padding: .7rem; border: none; border-radius: 10px;
background: var(--moss); color: #fff;
font-weight: 700; font-size: .9rem; cursor: pointer;
font-family: inherit;
}
.info-card .ic-btn:hover { background: #4a7a4e; }
/* === TOPBAR (sehr schlank) === */
.topbar {
position: fixed; top: 0; left: 0; right: 0; z-index: 20;
padding: .3rem .8rem;
display: flex; justify-content: space-between; align-items: center;
background: rgba(255,255,255,.75);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0,0,0,.08);
}
.topbar .brand {
display: flex; align-items: center; gap: .45rem;
font-weight: 800; font-size: .85rem; color: var(--fjord);
text-decoration: none;
}
.topbar .brand img { width: 22px; height: 22px; border-radius: 5px; }
/* === SAMMELHEFT (rechts oben) === */
.sammelheft {
position: fixed; top: 3.2rem; right: .65rem; z-index: 15;
background: rgba(255,255,255,.88);
backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,.6);
box-shadow: 0 8px 28px rgba(20,40,60,.18);
border-radius: 14px;
padding: .6rem .75rem;
width: 200px;
}
.sammelheft h4 {
font-size: .62rem; font-weight: 800; color: var(--fjord);
text-transform: uppercase; letter-spacing: .07em;
margin-bottom: .4rem;
}
.sh-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: .3rem;
}
.sh-slot {
width: 32px; height: 32px;
border-radius: 7px;
display: flex; align-items: center; justify-content: center;
font-size: 1.15rem;
background: rgba(0,0,0,.06);
border: 1.5px solid rgba(0,0,0,.06);
transition: all .2s;
}
.sh-slot.found { background: var(--moss-l); border-color: var(--moss); }
.sh-slot.empty { color: var(--text3); font-size: .85rem; }
.sh-progress {
margin-top: .45rem;
font-size: .62rem; font-weight: 700; color: var(--text2);
text-align: center;
}
.sh-bar { height: 4px; background: rgba(0,0,0,.08); border-radius: 2px; margin-top: .2rem; overflow: hidden; }
.sh-bar-fill { height: 100%; background: var(--moss); border-radius: 2px; transition: width .4s; }
/* === ZONEN-ANZEIGE (unten links, zeigt aktuelle Zone) === */
.zone-badge {
position: fixed; bottom: .8rem; left: .8rem; z-index: 15;
background: rgba(255,255,255,.82);
backdrop-filter: blur(10px);
border-radius: 12px; padding: .5rem .8rem;
box-shadow: 0 4px 16px rgba(0,0,0,.12);
font-size: .72rem; font-weight: 700; color: var(--text);
}
.zone-badge .zb-emoji { font-size: 1.1rem; margin-right: .3rem; }
.zone-badge .zb-name { color: var(--moss); }
/* === STEUERUNGS-HINT (unten mitte) === */
.controls-hint {
position: fixed; bottom: .8rem; left: 50%; transform: translateX(-50%);
z-index: 15;
background: rgba(255,255,255,.75);
backdrop-filter: blur(10px);
border-radius: 10px; padding: .4rem .8rem;
box-shadow: 0 4px 12px rgba(0,0,0,.1);
font-size: .65rem; color: var(--text2); font-weight: 600;
}
.controls-hint kbd {
display: inline-block; padding: 1px 5px;
background: rgba(0,0,0,.08); border: 1px solid rgba(0,0,0,.12);
border-radius: 3px; font-size: .62rem; font-weight: 800;
font-family: ui-monospace, monospace;
}
/* === GLÜCKWUNSCH-OVERLAY === */
.congrats-overlay {
position: fixed; inset: 0; z-index: 60;
background: rgba(15,35,25,.85); backdrop-filter: blur(10px);
display: flex; align-items: center; justify-content: center;
padding: 1.5rem;
}
.congrats-card {
background: #fff; border-radius: 20px; padding: 2rem 2.5rem;
max-width: 480px; text-align: center;
box-shadow: 0 30px 80px rgba(0,0,0,.4);
}
.congrats-card .cc-emoji { font-size: 4rem; margin-bottom: .5rem; }
.congrats-card h2 { font-size: 1.6rem; font-weight: 900; color: var(--fjord); margin-bottom: .5rem; }
.congrats-card p { font-size: .92rem; color: var(--text2); line-height: 1.6; margin-bottom: 1rem; }
.congrats-card .cc-stat { font-size: 1.1rem; font-weight: 800; color: var(--moss); margin-bottom: 1rem; }
.congrats-card .ic-btn { width: 100%; }
</style>
</head>
<body>
<!-- TOPBAR -->
<nav class="topbar">
<a href="index.html" class="brand">
<img src="assets/img/logo.png" alt="">
Forscher*in im Regenwald 🌴
</a>
<a href="index.html" class="brand" style="font-size:.7rem;opacity:.6;">← Zurück</a>
</nav>
<!-- SAMMELHEFT -->
<div class="sammelheft" id="sammelheft">
<h4>📔 Sammelheft</h4>
<div class="sh-grid" id="sh-grid"></div>
<div class="sh-progress" id="sh-progress">0 von 15 entdeckt</div>
<div class="sh-bar"><div class="sh-bar-fill" id="sh-bar-fill" style="width:0%"></div></div>
</div>
<!-- ZONEN-ANZEIGE -->
<div class="zone-badge" id="zone-badge">
<span class="zb-emoji">🏝</span>
<span class="zb-name">Flussmündung</span>
</div>
<!-- STEUERUNGS-HINT -->
<div class="controls-hint">
<kbd></kbd> flussaufwärts · <kbd></kbd> flussabwärts · Klick auf ein Tier zum Entdecken
</div>
<!-- FLUSS-SZENE -->
<div id="river-scene">
<div id="river-bed"></div>
<div class="tree-row left" id="trees-left"></div>
<div class="tree-row right" id="trees-right"></div>
<!-- Entdeckungs-Marker werden hier per JS eingefügt -->
</div>
<!-- BOOT -->
<div id="boat">🛶</div>
<!-- INFO-KARTE HOST -->
<div id="info-host"></div>
<!-- GLÜCKWUNSCH HOST -->
<div id="congrats-host"></div>
<script type="module">
import { DISCOVERIES, ZONES } from './src/sims/sim-11-regenwald/data.ts'
// ===================================================================
// STATE
// ===================================================================
const SAVE_KEY = 'regenwald-discovered'
let discovered = new Set()
try {
const saved = JSON.parse(localStorage.getItem(SAVE_KEY) || '[]')
if (Array.isArray(saved)) discovered = new Set(saved)
} catch {}
// Scroll-Position: 0 = Flussmündung (unten), 100 = Bergregenwald (oben)
let scrollPos = 0
const SCROLL_SPEED = 0.6
const SCENE_HEIGHT_VH = 350 // virtuelle Höhe der Szene in vh-Einheiten
function save() {
localStorage.setItem(SAVE_KEY, JSON.stringify([...discovered]))
}
// ===================================================================
// BÄUME AM UFER (deko, statisch)
// ===================================================================
function buildTrees() {
const treeEmojis = ['🌴', '🌳', '🌿', '🎋', '🌲']
const count = 35
for (const side of ['left', 'right']) {
const el = document.getElementById(`trees-${side}`)
let html = ''
for (let i = 0; i < count; i++) {
const emoji = treeEmojis[Math.floor(Math.random() * treeEmojis.length)]
const offset = side === 'left'
? `padding-right:${Math.random() * 30}px`
: `padding-left:${Math.random() * 30}px`
html += `<div style="${offset}">${emoji}</div>`
}
el.innerHTML = html
}
}
buildTrees()
// ===================================================================
// ENTDECKUNGS-MARKER
// ===================================================================
const scene = document.getElementById('river-scene')
const markerEls = []
for (const d of DISCOVERIES) {
const marker = document.createElement('div')
marker.className = 'discovery-marker'
marker.dataset.id = d.id
// Position: 0 = unten (100vh), 100 = oben (0vh)
// Wir setzen die Position relativ zur Szene-Höhe (SCENE_HEIGHT_VH)
const topPct = (1 - d.position / 100) * SCENE_HEIGHT_VH
marker.style.top = `${topPct}vh`
// Leicht links/rechts versetzen (abwechselnd)
const xOff = (DISCOVERIES.indexOf(d) % 2 === 0 ? -45 : 45)
marker.style.marginLeft = `${xOff}px`
const isFound = discovered.has(d.id)
marker.innerHTML = `
<div class="dm-bubble ${isFound ? 'found' : 'mystery'}">
${isFound ? d.emoji : '❓'}
</div>
<div class="dm-name">${isFound ? d.name : '???'}</div>
`
marker.addEventListener('click', () => showInfo(d))
scene.appendChild(marker)
markerEls.push({ marker, data: d })
}
// ===================================================================
// SAMMELHEFT
// ===================================================================
function renderSammelheft() {
const grid = document.getElementById('sh-grid')
grid.innerHTML = DISCOVERIES.map(d => {
const found = discovered.has(d.id)
return `<div class="sh-slot ${found ? 'found' : 'empty'}" title="${found ? d.name : '???'}">
${found ? d.emoji : '?'}
</div>`
}).join('')
const count = discovered.size
document.getElementById('sh-progress').textContent = `${count} von ${DISCOVERIES.length} entdeckt`
document.getElementById('sh-bar-fill').style.width = `${(count / DISCOVERIES.length) * 100}%`
}
renderSammelheft()
// ===================================================================
// ZONEN-BADGE
// ===================================================================
function updateZoneBadge() {
let zone = ZONES[0]
if (scrollPos > 33) zone = ZONES[1]
if (scrollPos > 66) zone = ZONES[2]
const badge = document.getElementById('zone-badge')
badge.innerHTML = `<span class="zb-emoji">${zone.emoji}</span> <span class="zb-name">${zone.name}</span> — ${zone.description}`
}
updateZoneBadge()
// ===================================================================
// SCROLL-MECHANIK (Pfeiltasten)
// ===================================================================
const keysDown = new Set()
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault()
keysDown.add(e.key)
}
})
window.addEventListener('keyup', (e) => keysDown.delete(e.key))
// Mausrad scrollt auch
window.addEventListener('wheel', (e) => {
e.preventDefault()
scrollPos = Math.max(0, Math.min(100, scrollPos + (e.deltaY > 0 ? -SCROLL_SPEED * 2 : SCROLL_SPEED * 2)))
applyScroll()
}, { passive: false })
function applyScroll() {
// Szene verschieben: 0 = Boden, 100 = Spitze
const translateY = scrollPos / 100 * (SCENE_HEIGHT_VH - 100)
scene.style.transform = `translateY(${translateY}vh)`
// Hintergrund-Gradient anpassen
const zone = scrollPos < 33 ? 0 : scrollPos < 66 ? 1 : 2
const z = ZONES[zone]
scene.style.background = `linear-gradient(0deg, ${z.bgFrom} 0%, ${z.bgTo} 100%)`
updateZoneBadge()
}
// rAF-Loop für Pfeiltasten-Scroll
function scrollLoop() {
if (keysDown.has('ArrowUp')) scrollPos = Math.min(100, scrollPos + SCROLL_SPEED)
if (keysDown.has('ArrowDown')) scrollPos = Math.max(0, scrollPos - SCROLL_SPEED)
if (keysDown.size > 0) applyScroll()
requestAnimationFrame(scrollLoop)
}
requestAnimationFrame(scrollLoop)
// ===================================================================
// INFO-KARTE
// ===================================================================
function showInfo(d) {
// Entdeckung registrieren
const wasNew = !discovered.has(d.id)
discovered.add(d.id)
save()
// Marker aktualisieren
const mel = markerEls.find(m => m.data.id === d.id)
if (mel) {
const bubble = mel.marker.querySelector('.dm-bubble')
bubble.className = 'dm-bubble found'
bubble.textContent = d.emoji
mel.marker.querySelector('.dm-name').textContent = d.name
}
renderSammelheft()
// Info-Overlay
const host = document.getElementById('info-host')
const zone = ZONES.find(z => z.id === d.zone)
host.innerHTML = `
<div class="info-overlay" id="info-bg">
<div class="info-card">
<div class="ic-emoji">${d.emoji}</div>
<h3>${d.name}</h3>
<div class="ic-zone">${zone?.emoji || ''} ${zone?.name || d.zone}</div>
<p>${d.fact}</p>
${d.wow ? `<div class="ic-wow">${d.wow}</div>` : ''}
<button class="ic-btn" id="info-close">
${wasNew ? '✅ Entdeckt! Weiter' : 'Verstanden ✓'}
</button>
</div>
</div>
`
document.getElementById('info-close')?.addEventListener('click', closeInfo)
document.getElementById('info-bg')?.addEventListener('click', (e) => {
if (e.target.id === 'info-bg') closeInfo()
})
// Prüfen ob alle gefunden
if (discovered.size === DISCOVERIES.length) {
setTimeout(showCongrats, 800)
}
}
function closeInfo() {
document.getElementById('info-host').innerHTML = ''
}
// ===================================================================
// GLÜCKWUNSCH
// ===================================================================
function showCongrats() {
const host = document.getElementById('congrats-host')
host.innerHTML = `
<div class="congrats-overlay">
<div class="congrats-card">
<div class="cc-emoji">🏆🌴</div>
<h2>Alle Tiere entdeckt!</h2>
<div class="cc-stat">${DISCOVERIES.length} von ${DISCOVERIES.length} gefunden</div>
<p>Du hast den ganzen Regenwald erforscht — von der Flussmündung bis zum Bergregenwald.</p>
<p>Aber vergiss nicht: Jedes Jahr verschwindet eine Regenwald-Fläche so groß wie die Schweiz. Was wir kennen, können wir schützen. 🌍</p>
<button class="ic-btn" onclick="this.closest('.congrats-overlay').remove()">Nochmal erkunden</button>
</div>
</div>
`
}
</script>
</body>
</html>