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>
This commit is contained in:
2026-04-26 01:05:13 +02:00
parent f811437a35
commit 79ac9dad84
773 changed files with 17482 additions and 541 deletions
+93 -31
View File
@@ -4,8 +4,8 @@
<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">
<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 {
@@ -56,7 +56,7 @@
/* === 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:450px; }
.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; }
@@ -76,13 +76,13 @@
.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; }
.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:50px; }
.graph-svg { width:100%; height:55px; display:block; }
/* === RIGHT: Measures Shop & Events === */
.right { display:flex; flex-direction:column; gap:.8rem; }
.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); }
@@ -310,37 +310,99 @@ function renderEvents() {
`).join('')
}
// === RENDER GRAPHS ===
// === 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
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')
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 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
}
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 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>
`
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 ===