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>
This commit is contained in:
2026-04-13 16:43:42 +02:00
commit f22c5ebbfe
83 changed files with 22709 additions and 0 deletions
+460
View File
@@ -0,0 +1,460 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Klimawächter</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 {
--bg: #f7f6f3; --bg2: #efeee9; --white: #fff;
--text: #1a1a1a; --text2: #4a4a4a; --text3: #8a8a8a;
--fjord: #4a7c8a; --fjord-l: #dae8ec; --fjord-d: #3a6470;
--moss: #5a8a5e; --moss-l: #dceadd;
--sand: #c4a35a; --sand-l: #f2eacc;
--coral: #c07a6b; --coral-l: #f2ddd8;
--danger: #b04a3a;
}
body { font-family:'Inter',system-ui,sans-serif; background:var(--bg); color:var(--text); line-height:1.5; }
.nav { padding:.6rem 1.2rem; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid rgba(0,0,0,.05); background:var(--white); }
.nav-brand { display:flex; align-items:center; gap:.5rem; font-weight:800; font-size:1rem; color:var(--fjord); text-decoration:none; }
.nav-brand img { width:28px; height:28px; border-radius:6px; }
.nav-actions { display:flex; gap:.4rem; }
.nav-btn { padding:.4rem .8rem; border:1px solid rgba(0,0,0,.08); border-radius:8px; background:var(--white); color:var(--text2); font-size:.78rem; font-weight:600; cursor:pointer; text-decoration:none; }
.nav-btn:hover { background:var(--fjord-l); border-color:var(--fjord); color:var(--fjord); }
/* === MAIN LAYOUT === */
.game-layout { display:grid; grid-template-columns:280px 1fr 280px; gap:.8rem; padding:.8rem; max-width:1400px; margin:0 auto; align-items:start; }
.panel { background:var(--white); border-radius:12px; border:1px solid rgba(0,0,0,.05); padding:.9rem; box-shadow:0 2px 8px rgba(0,0,0,.03); }
.panel-h { font-size:.66rem; font-weight:700; color:var(--fjord); text-transform:uppercase; letter-spacing:.06em; margin-bottom:.5rem; }
/* === LEFT: Resources & Goals === */
.left { display:flex; flex-direction:column; gap:.8rem; }
.resource { display:flex; align-items:center; gap:.6rem; padding:.4rem .2rem; border-bottom:1px solid rgba(0,0,0,.04); }
.resource:last-child { border-bottom:none; }
.resource-icon { font-size:1.4rem; width:28px; text-align:center; }
.resource-info { flex:1; min-width:0; }
.resource-name { font-size:.7rem; color:var(--text3); font-weight:500; }
.resource-val { font-size:.95rem; font-weight:700; color:var(--text); }
.resource-trend { font-size:.65rem; padding:1px 5px; border-radius:4px; font-weight:600; }
.trend-up { background:var(--moss-l); color:var(--moss); }
.trend-down { background:var(--coral-l); color:var(--coral); }
.goals .goal { padding:.4rem 0; border-bottom:1px solid rgba(0,0,0,.04); }
.goals .goal:last-child { border:0; }
.goal-row { display:flex; align-items:center; gap:.4rem; font-size:.78rem; }
.goal-check { width:14px; height:14px; border:1.5px solid var(--text3); border-radius:3px; display:flex; align-items:center; justify-content:center; font-size:.6rem; flex-shrink:0; }
.goal-check.done { background:var(--moss); border-color:var(--moss); color:#fff; }
.goal-bar { height:3px; background:#e8e5dc; border-radius:2px; margin-top:.3rem; overflow:hidden; }
.goal-bar-fill { height:100%; background:var(--moss); transition:width .3s; }
/* === CENTER: Game Canvas + Controls === */
.center { display:flex; flex-direction:column; gap:.6rem; }
.canvas-wrap { background:var(--white); border-radius:14px; border:1px solid rgba(0,0,0,.05); overflow:hidden; box-shadow:0 4px 16px rgba(0,0,0,.05); position:relative; }
/* Tutorial Overlay */
.tutorial-overlay { position:absolute; inset:0; background:rgba(15,30,40,.85); backdrop-filter:blur(8px); display:flex; align-items:center; justify-content:center; z-index:10; padding:1.5rem; }
.tutorial-card { background:var(--white); border-radius:16px; padding:1.5rem 1.8rem; max-width:480px; box-shadow:0 20px 60px rgba(0,0,0,.3); }
.tutorial-card h3 { font-size:1.2rem; font-weight:800; margin-bottom:.6rem; color:var(--fjord); }
.tutorial-card p { font-size:.88rem; color:var(--text2); white-space:pre-line; margin-bottom:1rem; }
.tutorial-card .progress-dots { display:flex; gap:.3rem; justify-content:center; margin:.8rem 0; }
.dot { width:8px; height:8px; border-radius:50%; background:#dadad0; }
.dot.active { background:var(--fjord); }
.tut-btn { width:100%; padding:.7rem; background:var(--fjord); color:#fff; border:none; border-radius:10px; font-weight:700; font-size:.9rem; cursor:pointer; }
.tut-btn:hover { background:var(--fjord-d); }
/* Speed controls */
.speed-bar { display:flex; gap:.3rem; padding:.5rem; background:var(--white); border-radius:10px; border:1px solid rgba(0,0,0,.05); }
.speed-btn { flex:1; padding:.5rem; border:none; background:transparent; color:var(--text2); font-size:.8rem; font-weight:600; cursor:pointer; border-radius:6px; }
.speed-btn:hover { background:#f0eeea; }
.speed-btn.active { background:var(--fjord); color:#fff; }
/* Graphs */
.graphs { display:grid; grid-template-columns:1fr 1fr; gap:.6rem; }
.graph-card { background:var(--white); border-radius:10px; border:1px solid rgba(0,0,0,.05); padding:.6rem; }
.graph-title { font-size:.65rem; color:var(--text3); font-weight:600; text-transform:uppercase; letter-spacing:.06em; margin-bottom:.2rem; }
.graph-svg { width:100%; height:50px; }
/* === RIGHT: Measures Shop & Events === */
.right { display:flex; flex-direction:column; gap:.8rem; }
.measure-card { display:flex; gap:.6rem; padding:.6rem; border-radius:10px; border:1px solid rgba(0,0,0,.06); margin-bottom:.4rem; cursor:pointer; transition:all .2s; background:var(--white); }
.measure-card:hover { border-color:var(--fjord); background:var(--fjord-l); }
.measure-card.disabled { opacity:.5; cursor:not-allowed; }
.measure-card.disabled:hover { border-color:rgba(0,0,0,.06); background:var(--white); }
.measure-icon { font-size:1.5rem; width:32px; text-align:center; }
.measure-info { flex:1; min-width:0; }
.measure-name { font-size:.8rem; font-weight:700; }
.measure-desc { font-size:.66rem; color:var(--text3); line-height:1.3; }
.measure-stats { display:flex; gap:.3rem; flex-wrap:wrap; margin-top:.2rem; }
.measure-stat { font-size:.6rem; padding:1px 5px; border-radius:4px; background:#f0eeea; color:var(--text3); font-weight:600; }
.measure-cost { background:var(--sand-l); color:var(--sand); }
.measure-co2 { background:var(--moss-l); color:var(--moss); }
.measure-protect { background:var(--fjord-l); color:var(--fjord); }
.measure-count { font-size:.62rem; padding:1px 5px; border-radius:4px; background:var(--fjord); color:#fff; font-weight:700; }
.events-list { max-height:160px; overflow-y:auto; }
.event { padding:.4rem .5rem; border-radius:8px; margin-bottom:.3rem; font-size:.74rem; line-height:1.3; }
.event-info { background:#f0eeea; color:var(--text2); }
.event-success { background:var(--moss-l); color:var(--moss); }
.event-warning { background:var(--sand-l); color:var(--sand); }
.event-danger { background:var(--coral-l); color:var(--danger); }
/* === END SCREEN === */
.end-screen { position:fixed; inset:0; background:rgba(15,30,40,.9); backdrop-filter:blur(10px); display:flex; align-items:center; justify-content:center; z-index:100; padding:2rem; }
.end-card { background:var(--white); border-radius:20px; padding:2rem 2.5rem; max-width:500px; text-align:center; box-shadow:0 30px 80px rgba(0,0,0,.4); }
.end-icon { font-size:4rem; margin-bottom:.5rem; }
.end-card h2 { font-size:1.8rem; font-weight:900; letter-spacing:-.02em; margin-bottom:.3rem; }
.end-card .end-summary { color:var(--text2); margin-bottom:1rem; }
.end-stats { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; margin:1rem 0; }
.end-stat { background:#f7f6f3; border-radius:10px; padding:.8rem; }
.end-stat-val { font-size:1.4rem; font-weight:800; color:var(--fjord); }
.end-stat-lbl { font-size:.7rem; color:var(--text3); text-transform:uppercase; }
/* RESPONSIVE */
@media (max-width: 1100px) {
.game-layout { grid-template-columns:1fr; }
.left, .right { order:2; }
}
</style>
</head>
<body>
<nav class="nav">
<a href="index.html" class="nav-brand"><img src="assets/img/logo.png" alt="">GeoGraSim · Klimawächter</a>
<div class="nav-actions">
<button class="nav-btn" id="btn-save">💾 Speichern</button>
<button class="nav-btn" id="btn-load">📂 Laden</button>
<button class="nav-btn" id="btn-finish">⏹ Beenden</button>
<button class="nav-btn" id="btn-reset" title="Stand löschen und neu starten">🔄 Reset</button>
<a href="index.html" class="nav-btn">← Zurück</a>
</div>
</nav>
<div class="game-layout">
<!-- LEFT: Resources, Goals -->
<aside class="left">
<div class="panel">
<div class="panel-h">📊 Status</div>
<div id="resources"></div>
</div>
<div class="panel goals">
<div class="panel-h">🎯 Ziele</div>
<div id="goals"></div>
</div>
</aside>
<!-- CENTER: Canvas + Controls + Graphs -->
<main class="center">
<div class="canvas-wrap" id="canvas-wrap">
<div class="tutorial-overlay" id="tutorial">
<div class="tutorial-card">
<h3 id="tut-title"></h3>
<p id="tut-text"></p>
<div class="progress-dots" id="tut-dots"></div>
<button class="tut-btn" id="tut-next">Weiter →</button>
</div>
</div>
</div>
<div class="speed-bar">
<button class="speed-btn" data-speed="0"></button>
<button class="speed-btn active" data-speed="1">▶ Normal</button>
<button class="speed-btn" data-speed="2">⏩ Schnell</button>
<button class="speed-btn" data-speed="4">⏭ Sehr schnell</button>
</div>
<div class="graphs">
<div class="graph-card">
<div class="graph-title">🌫 CO₂ über Zeit</div>
<svg class="graph-svg" id="graph-co2"></svg>
</div>
<div class="graph-card">
<div class="graph-title">🌡 Temperatur über Zeit</div>
<svg class="graph-svg" id="graph-temp"></svg>
</div>
<div class="graph-card">
<div class="graph-title">💰 Budget über Zeit</div>
<svg class="graph-svg" id="graph-budget"></svg>
</div>
<div class="graph-card">
<div class="graph-title">🌊 Meeresspiegel über Zeit</div>
<svg class="graph-svg" id="graph-sea"></svg>
</div>
</div>
</main>
<!-- RIGHT: Measures Shop, Events -->
<aside class="right">
<div class="panel">
<div class="panel-h">🏗 Maßnahmen kaufen</div>
<div id="measures"></div>
</div>
<div class="panel">
<div class="panel-h">📰 Ereignisse</div>
<div class="events-list" id="events"></div>
</div>
</aside>
</div>
<!-- End screen template -->
<div id="end-screen-host"></div>
<script type="module">
import { KlimawaechterGame, MEASURES } from './src/sims/sim-05-treibhaus/game.ts'
import { KlimawaechterRenderer } from './src/sims/sim-05-treibhaus/game-renderer.ts'
const game = new KlimawaechterGame()
let renderer = null
let endShown = false
// === ELEMENTS ===
const tutorialEl = document.getElementById('tutorial')
const tutTitle = document.getElementById('tut-title')
const tutText = document.getElementById('tut-text')
const tutDots = document.getElementById('tut-dots')
const tutNext = document.getElementById('tut-next')
const resourcesEl = document.getElementById('resources')
const goalsEl = document.getElementById('goals')
const measuresEl = document.getElementById('measures')
const eventsEl = document.getElementById('events')
const canvasWrap = document.getElementById('canvas-wrap')
// === RENDER MEASURES SHOP ===
function renderMeasures() {
const budget = game.getResource('budget')
measuresEl.innerHTML = MEASURES.map(m => {
const count = game.getMeasureCount(m.id)
const canAfford = budget >= m.cost
return `
<div class="measure-card ${canAfford ? '' : 'disabled'}" data-id="${m.id}">
<div class="measure-icon">${m.emoji}</div>
<div class="measure-info">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="measure-name">${m.name}</div>
${count > 0 ? `<span class="measure-count">×${count}</span>` : ''}
</div>
<div class="measure-desc">${m.description}</div>
<div class="measure-stats">
<span class="measure-stat measure-cost">💰 ${m.cost} €</span>
${m.co2Reduction > 0 ? `<span class="measure-stat measure-co2">${m.co2Reduction} CO₂/J</span>` : ''}
${m.protection > 0 ? `<span class="measure-stat measure-protect">🛡 +${m.protection} cm</span>` : ''}
<span class="measure-stat">⚙ ${m.upkeep} €/J</span>
</div>
</div>
</div>
`
}).join('')
measuresEl.querySelectorAll('.measure-card').forEach(el => {
el.addEventListener('click', () => {
if (el.classList.contains('disabled')) return
const id = el.dataset.id
game.buyMeasure(id)
})
})
}
// === RENDER RESOURCES ===
function renderResources() {
const resources = game.getResourcesArray()
resourcesEl.innerHTML = resources.map(r => `
<div class="resource">
<div class="resource-icon">${r.icon}</div>
<div class="resource-info">
<div class="resource-name">${r.name}</div>
<div class="resource-val">${r.format ? r.format(r.current) : r.current}</div>
</div>
</div>
`).join('')
}
// === RENDER GOALS ===
function renderGoals() {
const snap = game.getSnapshot()
goalsEl.innerHTML = snap.goals.map((g, i) => {
const goal = game.goals?.[i] || { title: g.id, description: '' }
return `
<div class="goal">
<div class="goal-row">
<div class="goal-check ${g.achieved ? 'done' : ''}">${g.achieved ? '✓' : ''}</div>
<div>${goal.title || g.id}</div>
</div>
<div class="goal-bar"><div class="goal-bar-fill" style="width:${g.progress}%"></div></div>
</div>
`
}).join('')
}
// === RENDER EVENTS ===
function renderEvents() {
const events = game.getEvents(8)
if (events.length === 0) {
eventsEl.innerHTML = '<div style="font-size:.72rem;color:var(--text3);text-align:center;padding:.5rem">Noch keine Ereignisse</div>'
return
}
eventsEl.innerHTML = events.map(e => `
<div class="event event-${e.severity}">
<strong>Jahr ${e.tick}:</strong> ${e.text}
</div>
`).join('')
}
// === RENDER GRAPHS ===
function renderGraphs() {
const snap = game.getSnapshot()
const tl = snap.timeline
if (tl.length === 0) return
drawGraph('graph-co2', tl.map(e => ({x: e.tick, y: e.values.co2 || 425})), 280, 800, '#8a7868')
drawGraph('graph-temp', tl.map(e => ({x: e.tick, y: e.values.temperature || 15})), 14, 22, '#c07a6b')
drawGraph('graph-budget', tl.map(e => ({x: e.tick, y: e.values.budget || 0})), -500, 3000, '#5a8a5e')
drawGraph('graph-sea', tl.map(e => ({x: e.tick, y: e.values.sealevel || 0})), 0, 200, '#4a7c8a')
}
function drawGraph(id, points, yMin, yMax, color) {
const svg = document.getElementById(id)
const w = svg.clientWidth || 200
const h = 50
svg.setAttribute('viewBox', `0 0 ${w} ${h}`)
if (points.length < 2) {
svg.innerHTML = ''
return
}
const maxX = Math.max(30, points[points.length - 1].x)
const path = points.map((p, i) => {
const x = (p.x / maxX) * w
const y = h - ((p.y - yMin) / (yMax - yMin)) * h
return `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${Math.max(2, Math.min(h-2, y)).toFixed(1)}`
}).join(' ')
const lastVal = points[points.length - 1].y
svg.innerHTML = `
<path d="${path}" fill="none" stroke="${color}" stroke-width="2" stroke-linejoin="round" />
<text x="${w-3}" y="12" text-anchor="end" font-size="9" font-weight="700" fill="${color}">${lastVal.toFixed(lastVal > 50 ? 0 : 1)}</text>
`
}
// === TUTORIAL ===
function renderTutorial() {
const step = game.getCurrentTutorialStep()
if (!step || game.getSnapshot().state !== 'tutorial') {
tutorialEl.style.display = 'none'
if (!renderer) {
renderer = new KlimawaechterRenderer(canvasWrap, game)
renderer.start()
}
return
}
tutorialEl.style.display = 'flex'
tutTitle.textContent = step.title
tutText.textContent = step.text
const totalSteps = 4
tutDots.innerHTML = ''
for (let i = 0; i < totalSteps; i++) {
const d = document.createElement('div')
d.className = 'dot' + (i === game.getSnapshot().tutorialStep ? ' active' : '')
tutDots.appendChild(d)
}
tutNext.textContent = game.getSnapshot().tutorialStep === totalSteps - 1 ? 'Simulation starten 🚀' : 'Weiter →'
}
tutNext.addEventListener('click', () => {
game.nextTutorialStep()
renderAll()
})
// === SPEED CONTROL ===
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.addEventListener('click', () => {
const speed = parseInt(btn.dataset.speed)
game.setSpeed(speed)
document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'))
btn.classList.add('active')
})
})
// === SAVE / LOAD / FINISH ===
document.getElementById('btn-save').addEventListener('click', () => {
localStorage.setItem('klimawaechter-save', game.serialize())
alert('💾 Stand gespeichert')
})
document.getElementById('btn-load').addEventListener('click', () => {
const save = localStorage.getItem('klimawaechter-save')
if (!save) { alert('Kein Stand vorhanden'); return }
if (game.deserialize(save)) {
renderAll()
alert('📂 Stand geladen')
}
})
document.getElementById('btn-finish').addEventListener('click', () => {
game.finish()
showEndScreen()
})
// === END SCREEN ===
function showEndScreen() {
if (endShown) return
endShown = true
const snap = game.getSnapshot()
const won = snap.state === 'won'
const lost = snap.state === 'lost'
const icon = won ? '🏆' : lost ? '💔' : '🏁'
const title = won ? 'Du hast gewonnen!' : lost ? 'Simulation gescheitert' : 'Simulation beendet'
const summary = won
? 'Glückwunsch! Du hast deine Stadt bis zum Jahr 2100 erfolgreich durch die Klimakrise geführt.'
: lost
? snap.resources.budget < 0 ? 'Deine Stadt ist pleite gegangen.' : 'Der steigende Meeresspiegel hat zu viele Häuser zerstört.'
: `Du hast im Jahr ${2025 + snap.tick} beendet.`
const html = `
<div class="end-screen">
<div class="end-card">
<div class="end-icon">${icon}</div>
<h2>${title}</h2>
<p class="end-summary">${summary}</p>
<div class="end-stats">
<div class="end-stat"><div class="end-stat-val">${snap.tick}</div><div class="end-stat-lbl">Jahre (bis ${2025 + snap.tick})</div></div>
<div class="end-stat"><div class="end-stat-val">${Math.round(snap.resources.budget)} €</div><div class="end-stat-lbl">Budget übrig</div></div>
<div class="end-stat"><div class="end-stat-val">${snap.resources.temperature.toFixed(1)}°C</div><div class="end-stat-lbl">Endtemperatur</div></div>
<div class="end-stat"><div class="end-stat-val">${Math.round(snap.resources.flooded)}%</div><div class="end-stat-lbl">Überflutet</div></div>
</div>
<button class="tut-btn" onclick="location.reload()">🔄 Nochmal spielen</button>
</div>
</div>
`
document.getElementById('end-screen-host').innerHTML = html
}
// === MAIN RENDER ===
function renderAll() {
renderResources()
renderGoals()
renderMeasures()
renderEvents()
renderGraphs()
renderTutorial()
const snap = game.getSnapshot()
if ((snap.state === 'won' || snap.state === 'lost') && !endShown) {
setTimeout(showEndScreen, 800)
}
}
// Subscribe to game updates
game.subscribe(renderAll)
// Initial render
renderAll()
</script>
</body>
</html>