Files
geograsim/App/game.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

461 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>