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:
+93
-31
@@ -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 ===
|
||||
|
||||
Reference in New Issue
Block a user