Files
geograsim/App/game.html
T
Adminator 79ac9dad84 Atlas: Deploy-Buendel vor Staustufen-Sprint
- Logistik: Musik-Player Playlist-Modus (5 Gruppen, 20 Tracks)
- Heli: End-Screen auf .ggs-endscreen, Mission-Bilder, Voice-Lines, Briefing-Audio
- Logistik: Layout-Tausch Auftraege+Fahrzeuge links, Karte rechts
- Logistik: Tier-1-Staedte ausgebaut, Auto-Timescale-Badge
- Logistik: Roadnet/Railnet Dijkstra-Routing, Balance-Updates
- Atlas: 5 Atlas-Inbox-Nachrichten (Cards-Pattern, DALL-E-Key, Musik-Playlists)
- Status-Updates Heli + Logistik

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:05:13 +02:00

523 lines
23 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="favicon-96x96.png" sizes="96x96"><link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="stylesheet" href="assets/fonts/inter.css">
<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:visible; box-shadow:0 4px 16px rgba(0,0,0,.05); position:relative; min-height:460px; }
/* 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:.5rem; margin-top:.4rem; }
.graph-card { background:var(--white); border-radius:10px; border:1px solid rgba(0,0,0,.05); padding:.5rem .6rem .3rem; }
.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:55px; display:block; }
/* === RIGHT: Measures Shop & Events === */
.right { display:flex; flex-direction:column; gap:.8rem; max-height:50vh; overflow-y:auto; }
.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 (volle Version mit Zonen, Referenzlinien, Animation) ===
const GRAPH_CFG = [
{id:'graph-co2', key:'co2', color:'#8a7868', unit:'ppm', label:'CO₂',
zones:[{min:280,max:350,color:'rgba(90,138,94,.12)',label:'Vorindustriell'},{min:350,max:450,color:'rgba(196,163,90,.1)'},{min:450,max:800,color:'rgba(192,122,107,.08)'}],
lines:[{y:420,label:'Heute',color:'#c4a35a',dash:true}]},
{id:'graph-temp', key:'temperature', color:'#c07a6b', unit:'°C', label:'Temperatur',
zones:[{min:14,max:16,color:'rgba(90,138,94,.12)',label:'Sicher'},{min:16,max:17,color:'rgba(196,163,90,.1)'},{min:17,max:22,color:'rgba(192,122,107,.08)'}],
lines:[{y:17,label:'Klimaziel',color:'#c07a6b',dash:true}]},
{id:'graph-budget', key:'budget', color:'#5a8a5e', unit:'Mrd', label:'Budget',
zones:[], lines:[{y:0,label:'',color:'#ccc',dash:true}]},
{id:'graph-sea', key:'sealevel', color:'#4a7c8a', unit:'cm', label:'Meeresspiegel',
zones:[{min:0,max:30,color:'rgba(90,138,94,.08)'},{min:30,max:100,color:'rgba(196,163,90,.08)'},{min:100,max:300,color:'rgba(192,122,107,.08)'}],
lines:[{y:50,label:'Kritisch',color:'#c07a6b',dash:true}]}
]
function renderGraphs() {
const snap = game.getSnapshot()
const tl = snap.timeline
if (tl.length === 0) return
GRAPH_CFG.forEach(cfg => {
const pts = tl.filter(e => e.values).map(e => ({x: e.tick, y: e.values[cfg.key] || 0}))
// Append live value
const live = snap.values?.[cfg.key]
if (live !== undefined) pts.push({x: snap.tick, y: live})
drawGraph(cfg, pts)
})
}
function niceFloor(v, step) { return Math.floor(v / step) * step }
function niceCeil(v, step) { return Math.ceil(v / step) * step }
function fmtTick(v) { return Math.abs(v) >= 1000 ? (v/1000).toFixed(1)+'k' : v >= 10 ? v.toFixed(0) : v.toFixed(1) }
function drawGraph(cfg, points) {
const svg = document.getElementById(cfg.id)
if (!svg) return
const W = svg.clientWidth || 200, H = 55
const pad = {l:28, r:6, t:4, b:12}
const gw = W - pad.l - pad.r, gh = H - pad.t - pad.b
svg.setAttribute('viewBox', `0 0 ${W} ${H}`)
if (points.length < 2) { svg.innerHTML = ''; return }
// Auto-scale Y
let allY = points.map(p => p.y)
let yMin = Math.min(...allY), yMax = Math.max(...allY)
const margin = (yMax - yMin) * 0.1 || 1
const step = yMax - yMin > 100 ? 50 : yMax - yMin > 10 ? 5 : 1
yMin = niceFloor(yMin - margin, step)
yMax = niceCeil(yMax + margin, step)
if (yMin === yMax) { yMin -= 1; yMax += 1 }
const maxX = Math.max(30, points[points.length - 1].x)
const px = p => pad.l + (p.x / maxX) * gw
const py = p => pad.t + gh - ((p.y - yMin) / (yMax - yMin)) * gh
let html = ''
// Zones
;(cfg.zones || []).forEach(z => {
const zy1 = pad.t + gh - ((Math.min(z.max, yMax) - yMin) / (yMax - yMin)) * gh
const zy2 = pad.t + gh - ((Math.max(z.min, yMin) - yMin) / (yMax - yMin)) * gh
html += `<rect x="${pad.l}" y="${zy1}" width="${gw}" height="${zy2-zy1}" fill="${z.color}" />`
})
// Reference lines
;(cfg.lines || []).forEach(ln => {
if (ln.y >= yMin && ln.y <= yMax) {
const ly = pad.t + gh - ((ln.y - yMin) / (yMax - yMin)) * gh
html += `<line x1="${pad.l}" y1="${ly}" x2="${W-pad.r}" y2="${ly}" stroke="${ln.color}" stroke-width=".7" ${ln.dash ? 'stroke-dasharray="3,2"' : ''} />`
if (ln.label) html += `<text x="${W-pad.r-1}" y="${ly-2}" text-anchor="end" font-size="5.5" fill="${ln.color}" opacity=".7">${ln.label}</text>`
}
})
// Y-axis ticks
for (let i = 0; i <= 3; i++) {
const val = yMin + (yMax - yMin) * (i / 3)
const ty = pad.t + gh - (i / 3) * gh
html += `<text x="${pad.l-2}" y="${ty+2.5}" text-anchor="end" font-size="5.5" fill="#aaa">${fmtTick(val)}</text>`
html += `<line x1="${pad.l}" y1="${ty}" x2="${W-pad.r}" y2="${ty}" stroke="#eee" stroke-width=".3" />`
}
// Data line
const path = points.map((p, i) => `${i===0?'M':'L'} ${px(p).toFixed(1)} ${Math.max(pad.t+1, Math.min(H-pad.b-1, py(p))).toFixed(1)}`).join(' ')
html += `<path d="${path}" fill="none" stroke="${cfg.color}" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round" />`
// Animated dot at latest point
const last = points[points.length - 1]
const lx = px(last), ly = Math.max(pad.t+1, Math.min(H-pad.b-1, py(last)))
html += `<circle cx="${lx}" cy="${ly}" r="2.5" fill="${cfg.color}"><animate attributeName="r" values="2.5;4;2.5" dur="1.5s" repeatCount="indefinite"/><animate attributeName="opacity" values="1;.5;1" dur="1.5s" repeatCount="indefinite"/></circle>`
// Value badge
html += `<text x="${W-pad.r}" y="${pad.t+7}" text-anchor="end" font-size="7" font-weight="700" fill="${cfg.color}">${last.y.toFixed(last.y > 50 ? 0 : 1)} ${cfg.unit}</text>`
svg.innerHTML = html
}
// === 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>