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

1240 lines
50 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 3D</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;
--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;
--glass-bg: rgba(255,255,255,.80);
--glass-bg2: rgba(255,255,255,.90);
--glass-bord: rgba(255,255,255,.55);
--glass-shadow: 0 8px 32px rgba(20,40,60,.18);
}
html, body { height:100%; overflow:hidden; }
body { font-family:'Inter',system-ui,sans-serif; background:#bcd5e6; color:var(--text); line-height:1.5; }
/* === FULLSCREEN CANVAS === */
#canvas-wrap { position:fixed; inset:0; z-index:0; background:#bcd5e6; }
#canvas-wrap canvas { width:100% !important; height:100% !important; display:block; }
/* === FLOATING PANELS === */
.floating {
position:fixed; z-index:5;
backdrop-filter:blur(14px) saturate(140%); -webkit-backdrop-filter:blur(14px) saturate(140%);
background:var(--glass-bg); border:1px solid var(--glass-bord);
box-shadow:var(--glass-shadow); border-radius:14px;
}
.floating-strong { background:var(--glass-bg2); }
/* === TOP BAR — durchgehende Leiste oben === */
.topbar {
top: 0; left: 0; right: 0;
padding: .35rem .7rem;
display: flex; justify-content: space-between; align-items: center; gap: .8rem;
border-radius: 0; /* keine Card mehr */
border-left: none; border-right: none; border-top: none;
border-bottom: 1px solid rgba(0,0,0,.08);
}
.topbar .brand {
display:flex; align-items:center; gap:.5rem; font-weight:800; font-size:.92rem;
color:var(--fjord); text-decoration:none; white-space:nowrap;
}
.topbar .brand img { width:24px; height:24px; border-radius:6px; }
.topbar .brand .badge-3d {
background:linear-gradient(135deg,#4a7c8a,#6aa8b8); color:#fff;
font-size:.58rem; padding:2px 6px; border-radius:4px;
font-weight:800; letter-spacing:.04em;
}
.topbar-controls { display:flex; gap:.5rem; align-items:center; }
/* Icon-only Buttons im Header */
.icon-btn, .nav-btn {
width:32px; height:32px; border-radius:8px;
border:1px solid rgba(0,0,0,.08); background:rgba(255,255,255,.55);
color:var(--text2); font-size:.95rem; cursor:pointer;
display:flex; align-items:center; justify-content:center; padding:0;
text-decoration:none; transition:all .15s;
}
.icon-btn:hover, .nav-btn:hover {
background:var(--fjord-l); border-color:var(--fjord); color:var(--fjord);
}
.ctrl-group {
display:flex; gap:.2rem; padding:.2rem;
background:rgba(255,255,255,.45); border-radius:9px;
border:1px solid rgba(0,0,0,.06);
}
.ctrl-group .icon-btn {
border:none; background:transparent;
width:30px; height:30px; font-size:.8rem; font-weight:700;
}
.ctrl-group .icon-btn:hover { background:rgba(0,0,0,.06); }
.ctrl-group .icon-btn.active {
background:var(--fjord); color:#fff;
}
.speed-label { font-size:.55rem; font-weight:800; opacity:.85; }
.topbar-sep {
width:1px; height:24px; background:rgba(0,0,0,.08);
}
/* === PANELS — schmaler als vorher === */
.panel-h {
font-size:.58rem; font-weight:800; color:var(--fjord);
text-transform:uppercase; letter-spacing:.07em; margin-bottom:.4rem;
}
/*
* === LAYOUT-KONZEPT ===
* Drei feste Bereiche, alle mit denselben Abständen:
* 1. Topbar oben (durchgehende Leiste, kein Abstand)
* 2. Graphen-Leiste unten quer, volle Breite (--graphs-h)
* 3. Seitenpanels rechts und links DAZWISCHEN
*
* Jede Spalte hat 2 Panels (oben/unten) mit klarem Trennabstand --gap.
*/
:root {
--topbar-h: 2.8rem; /* Höhe der Topbar-Leiste */
--graphs-h: 23vh; /* Höhe der Graphen-Leiste */
--side-w: 210px; /* Breite der linken Status-Spalte */
--shop-w: 270px; /* Breite der rechten Maßnahmen-Spalte (breiter) */
--pad: .65rem; /* Außenabstand */
--gap: .55rem; /* Abstand zwischen den 2 Panels einer Spalte */
}
/* LINKS: eine durchgehende Spalte mit Status + Ziele + Ereignisse */
.panel-status {
top: calc(var(--topbar-h) + var(--pad));
bottom: calc(var(--graphs-h) + var(--pad) + var(--pad));
left: var(--pad);
width: var(--side-w);
padding: .6rem .75rem;
overflow-y: auto;
}
/* Zweite/dritte Überschrift in der Status-Box (Ziele, Ereignisse) */
.panel-h-sub {
margin-top: .9rem;
padding-top: .6rem;
border-top: 1px solid rgba(0,0,0,.08);
}
/* RECHTS: eine durchgehende Spalte für Maßnahmen, breiter */
.panel-shop {
top: calc(var(--topbar-h) + var(--pad));
bottom: calc(var(--graphs-h) + var(--pad) + var(--pad));
right: var(--pad);
width: var(--shop-w);
padding: .6rem .75rem;
overflow-y: auto;
}
/* === GRAPHEN-LEISTE — unten quer, volle Breite === */
.panel-graphs {
bottom: var(--pad);
left: var(--pad);
right: var(--pad);
height: var(--graphs-h);
padding: .5rem .65rem;
}
.graphs-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: .6rem;
height: 100%;
}
.graph-card {
background: rgba(255,255,255,.62);
border-radius: 9px;
padding: .35rem .55rem .35rem;
border: 1px solid rgba(0,0,0,.05);
display: flex; flex-direction: column;
min-height: 0; min-width: 0;
}
.graph-title {
font-size: .62rem; color: var(--text3); font-weight: 700;
text-transform: uppercase; letter-spacing: .05em;
display: flex; justify-content: space-between; align-items: center;
margin-bottom: .15rem;
flex-shrink: 0;
}
.graph-info-btn {
width: 14px; height: 14px; border-radius: 50%; border: none;
background: rgba(0,0,0,.08); color: var(--text2);
font-size: .58rem; font-weight: 800; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.graph-info-btn:hover { background: var(--fjord); color: #fff; }
.graph-svg {
width: 100%; flex: 1; min-height: 0; display: block;
}
/* === GRAPH-CLICK-ZOOM === */
.graph-card { cursor: zoom-in; }
.graph-card.zoomed {
cursor: zoom-out;
box-shadow: 0 30px 80px rgba(0,0,0,.45);
background: rgba(255,255,255,.97);
backdrop-filter: blur(20px) saturate(140%);
border: 1px solid rgba(255,255,255,.6);
padding: 1.1rem 1.4rem;
}
/* Im Zoom Title etwas größer (war .62rem), aber NICHT 1rem */
.graph-card.zoomed .graph-title { font-size: .78rem; margin-bottom: .55rem; }
.graph-card.zoomed .unit-chip { font-size: .9em; }
.graph-zoom-backdrop {
position: fixed; inset: 0; z-index: 140;
background: rgba(15,30,40,.55); backdrop-filter: blur(6px);
opacity: 0; transition: opacity .35s ease-out;
pointer-events: none;
}
.graph-zoom-backdrop.active {
opacity: 1; pointer-events: auto;
}
/* Einheiten-Chip im Graph-Titel mit Tooltip */
.unit-chip {
display: inline-flex; align-items: center; gap: .22em;
background: rgba(0,0,0,.06); color: var(--text2);
padding: 1px 6px 1px 6px; border-radius: 5px;
text-transform: none; letter-spacing: 0;
font-size: .88em; font-weight: 700; cursor: help;
margin-left: .15em;
transition: background .12s;
}
.unit-chip:hover { background: var(--fjord-l); color: var(--fjord-d); }
.unit-chip .unit-i {
display: inline-flex; align-items: center; justify-content: center;
width: 11px; height: 11px; border-radius: 50%;
background: rgba(0,0,0,.28); color: #fff;
font-size: .56em; font-weight: 900;
font-style: italic;
}
.unit-chip:hover .unit-i { background: var(--fjord); }
/* === STATUS (LEFT) === */
.resource {
display:flex; align-items:flex-start; gap:.5rem; padding:.3rem .1rem;
border-bottom:1px solid rgba(0,0,0,.06);
}
.resource:last-child { border-bottom:none; }
.resource-icon { font-size:1.15rem; width:22px; text-align:center; padding-top:.1rem; }
.resource-info { flex:1; min-width:0; }
.resource-name { font-size:.58rem; color:var(--text3); font-weight:500; line-height:1.1; }
.resource-val { font-size:.85rem; font-weight:800; color:var(--text); line-height:1.2; }
/* === STROM-NETZ-VISUALISIERUNG === */
.power-grid {
margin-top:.35rem;
padding:.4rem .45rem;
background:rgba(0,0,0,.04);
border-radius:7px;
display:flex; flex-direction:column; gap:.28rem;
}
.pg-row { display:flex; align-items:center; gap:.35rem; }
.pg-lbl {
font-size:.56rem; font-weight:700;
color:var(--text3); white-space:nowrap; min-width:48px;
}
.pg-track {
flex:1; height:8px;
background:rgba(0,0,0,.10);
border-radius:4px; overflow:hidden;
position:relative;
}
.pg-fill {
height:100%; border-radius:4px;
transition:width .35s ease-out, background .25s;
}
.pg-cap { background:linear-gradient(90deg, #5a8a5e, #6aa86e); }
.pg-cap-low { background:linear-gradient(90deg, #b04a3a, #c07a6b); }
.pg-dem { background:linear-gradient(90deg, #4a7c8a, #6aa8b8); }
.pg-num {
font-size:.6rem; font-weight:800; color:var(--text);
min-width:36px; text-align:right;
}
.pg-status {
font-size:.56rem; font-weight:700;
text-align:center; padding:.18rem 0 0;
letter-spacing:.02em;
}
.pg-status-ok { color:var(--moss); }
.pg-status-bad { color:var(--danger); animation:pg-pulse 1.4s ease-in-out infinite; }
@keyframes pg-pulse { 0%,100%{opacity:1} 50%{opacity:.55} }
/* === BAUMODUS-HINWEIS-BANNER === */
/* Schwebt mittig oben unter der Topbar, zeigt was gerade gebaut wird
und wie man den Modus beendet. Sichtbar nur wenn .visible. */
.build-banner {
position: fixed;
top: calc(var(--topbar-h) + var(--pad));
left: 50%;
transform: translate(-50%, -12px);
z-index: 30;
padding: .55rem 1rem;
background: rgba(255,255,255,.92);
backdrop-filter: blur(14px);
border: 1px solid var(--fjord);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(20,40,60,.22);
display: flex; align-items: center; gap: .6rem;
font-size: .8rem; font-weight: 700; color: var(--text);
pointer-events: none;
opacity: 0;
transition: opacity .25s ease-out, transform .25s ease-out;
}
.build-banner.visible {
opacity: 1;
transform: translate(-50%, 0);
}
.build-banner .bb-emoji { font-size: 1.2rem; }
.build-banner .bb-text { line-height: 1.25; }
.build-banner .bb-text .bb-name { color: var(--fjord); }
.build-banner .bb-text .bb-hint { font-size: .65rem; font-weight: 600; color: var(--text3); }
.build-banner kbd {
display: inline-block; padding: 1px 6px;
background: rgba(0,0,0,.08); border: 1px solid rgba(0,0,0,.15);
border-radius: 4px; font-size: .65rem; font-weight: 800;
font-family: ui-monospace, monospace;
color: var(--text);
}
/* === DIFFICULTY SELECTOR === */
.difficulty-overlay {
position: fixed; inset: 0; z-index: 200;
background: rgba(15,30,40,.88); backdrop-filter: blur(10px);
display: flex; align-items: center; justify-content: center;
padding: 1.5rem;
}
.difficulty-card {
background: #fff; border-radius: 18px;
padding: 1.8rem 2rem; max-width: 720px; width: 100%;
box-shadow: 0 30px 80px rgba(0,0,0,.4);
}
.difficulty-card h2 {
font-size: 1.4rem; font-weight: 800; margin-bottom: .35rem;
color: var(--fjord); letter-spacing: -.01em;
}
.difficulty-card p.subtitle {
color: var(--text2); font-size: .95rem; margin-bottom: 1.4rem;
}
.difficulty-buttons {
display: grid; grid-template-columns: repeat(3, 1fr); gap: .9rem;
}
.difficulty-btn {
background: #f7f6f3; border: 2px solid rgba(0,0,0,.08);
border-radius: 14px; padding: 1.1rem 1rem 1rem;
cursor: pointer; transition: all .15s;
display: flex; flex-direction: column; align-items: center;
gap: .5rem; text-align: center; font-family: inherit;
}
.difficulty-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(0,0,0,.12);
}
.difficulty-btn.lvl-1 { border-color: rgba(90,138,94,.35); }
.difficulty-btn.lvl-1:hover { border-color: var(--moss); background: var(--moss-l); }
.difficulty-btn.lvl-2 { border-color: rgba(196,163,90,.35); }
.difficulty-btn.lvl-2:hover { border-color: var(--sand); background: var(--sand-l); }
.difficulty-btn.lvl-3 { border-color: rgba(176,74,58,.35); }
.difficulty-btn.lvl-3:hover { border-color: var(--danger); background: var(--coral-l); }
.difficulty-emoji { font-size: 2.4rem; line-height: 1; }
.difficulty-name { font-size: 1.05rem; font-weight: 800; color: var(--text); }
.difficulty-desc { font-size: .76rem; color: var(--text2); line-height: 1.45; }
.difficulty-stats {
font-size: .65rem; color: var(--text3); margin-top: .35rem;
font-variant-numeric: tabular-nums;
}
/* === LEVEL BADGE in der Topbar === */
.level-badge {
display: flex; align-items: center; gap: .35rem;
padding: 4px 9px; border-radius: 8px;
background: rgba(255,255,255,.55); border: 1px solid rgba(0,0,0,.08);
font-size: .68rem; font-weight: 800; color: var(--text2);
cursor: pointer; transition: all .12s;
font-family: inherit;
}
.level-badge:hover { background: var(--fjord-l); border-color: var(--fjord); color: var(--fjord); }
.level-badge .lb-emoji { font-size: .85rem; }
/* === BUDGET-BILANZ === */
.budget-balance {
margin-top:.35rem;
padding:.35rem .45rem;
background:rgba(0,0,0,.04);
border-radius:7px;
display:flex; flex-direction:column; gap:.18rem;
}
.bb-row {
display:flex; justify-content:space-between; align-items:center;
font-size:.6rem; line-height:1.15;
}
.bb-lbl { color:var(--text3); font-weight:600; }
.bb-num { font-weight:800; font-variant-numeric:tabular-nums; }
.bb-pos { color:var(--moss); }
.bb-neg { color:var(--danger); }
.bb-net-row {
margin-top:.15rem; padding-top:.22rem;
border-top:1px solid rgba(0,0,0,.10);
}
.bb-net-row .bb-lbl { color:var(--text2); font-weight:700; }
.bb-net { font-size:.68rem; }
/* === GOALS === */
.goal { padding:.3rem 0; border-bottom:1px solid rgba(0,0,0,.06); }
.goal:last-child { border:0; }
.goal-row { display:flex; align-items:center; gap:.35rem; font-size:.7rem; }
.goal-check {
width:12px; height:12px; border:1.5px solid var(--text3);
border-radius:3px; display:flex; align-items:center; justify-content:center;
font-size:.52rem; flex-shrink:0;
}
.goal-check.done { background:var(--moss); border-color:var(--moss); color:#fff; }
.goal-bar { height:3px; background:rgba(0,0,0,.08); border-radius:2px; margin-top:.2rem; overflow:hidden; }
.goal-bar-fill { height:100%; background:var(--moss); transition:width .3s; }
/* === SHOP === */
.measure-card {
display:flex; gap:.5rem; padding:.5rem; border-radius:9px;
border:1px solid rgba(0,0,0,.08); margin-bottom:.35rem; cursor:pointer;
transition:all .15s; background:rgba(255,255,255,.55);
}
.measure-card:hover { border-color:var(--fjord); background:var(--fjord-l); transform:translateX(-2px); }
.measure-card.disabled { opacity:.45; cursor:not-allowed; }
.measure-card.disabled:hover { border-color:rgba(0,0,0,.08); background:rgba(255,255,255,.55); transform:none; }
.measure-icon { font-size:1.25rem; width:26px; text-align:center; }
.measure-info { flex:1; min-width:0; }
.measure-name { font-size:.72rem; font-weight:700; }
.measure-desc { font-size:.58rem; color:var(--text3); line-height:1.3; }
.measure-stats { display:flex; gap:.22rem; flex-wrap:wrap; margin-top:.18rem; }
.measure-stat {
font-size:.54rem; padding:1px 5px; border-radius:4px;
background:rgba(0,0,0,.05); color:var(--text3); font-weight:600;
}
.measure-cost { background:var(--sand-l); color:var(--sand); }
.measure-reduction { background:var(--moss-l); color:var(--moss); }
.measure-protection { background:var(--fjord-l); color:var(--fjord); }
.measure-capacity { background:#e8e3f2; color:#7a5ea8; }
.measure-quality { background:var(--coral-l); color:var(--danger); }
.measure-neutral { background:rgba(0,0,0,.05); color:var(--text3); }
/* Stückzahl-Badge im Maßnahmen-Namen */
.measure-owned {
display:inline-block; margin-left:.25rem;
background:var(--fjord); color:#fff;
font-size:.55rem; font-weight:800;
padding:1px 5px; border-radius:8px;
vertical-align:middle;
}
/* Abreißen-Button rechts auf der Karte */
.measure-card { position:relative; }
.measure-demolish {
position:absolute; top:.35rem; right:.35rem;
width:20px; height:20px; border:none; cursor:pointer;
background:rgba(0,0,0,.06); border-radius:5px;
font-size:.72rem; line-height:1;
display:flex; align-items:center; justify-content:center;
color:var(--text2); transition:all .12s;
}
.measure-demolish:hover { background:var(--coral-l); color:var(--danger); }
/* === EVENTS-STRIP (rechts vom Status-Panel, klappt nach oben auf) ===
Sitzt unten direkt rechts vom Status-Panel, gleiche Breite.
Default: nur Header + 1 Card (~80px hoch). Klick → max-height wächst
nach oben, zeigt alle Ereignisse scrollbar. Klick wieder → schließt. */
.panel-events-strip {
position: fixed;
left: calc(var(--side-w) + var(--pad) * 2);
bottom: calc(var(--graphs-h) + var(--pad) + var(--pad));
width: var(--side-w);
max-height: 84px;
padding: .35rem .55rem .45rem;
overflow: hidden;
cursor: pointer;
transition: max-height .38s cubic-bezier(.22,1,.36,1),
box-shadow .25s ease-out;
z-index: 12;
display: flex; flex-direction: column; gap: .25rem;
}
.panel-events-strip.open {
max-height: calc(100vh - var(--topbar-h) - var(--graphs-h) - 4 * var(--pad));
box-shadow: 0 18px 50px rgba(20,40,60,.28);
cursor: zoom-out;
}
.events-strip-header {
display: flex; justify-content: space-between; align-items: center;
font-size: .58rem; font-weight: 800; color: var(--fjord);
text-transform: uppercase; letter-spacing: .07em;
flex-shrink: 0;
}
.esh-toggle {
display: inline-block; transition: transform .35s ease-out;
font-size: .65rem; color: var(--text3);
}
.panel-events-strip.open .esh-toggle { transform: rotate(180deg); }
.events-strip-list {
overflow-y: auto;
flex: 1; min-height: 0;
}
/* Im geschlossenen Zustand: Liste zeigt nur das erste (neueste) Element */
.panel-events-strip:not(.open) .events-strip-list .event:not(:first-child) {
display: none;
}
/* Im geschlossenen Zustand: Card kompakt halten */
.panel-events-strip:not(.open) .events-strip-list .event {
margin-bottom: 0;
}
/* === EVENTS === */
.event {
padding:.3rem .45rem; border-radius:7px; margin-bottom:.25rem;
font-size:.68rem; line-height:1.3;
display:flex; gap:.3rem; align-items:flex-start;
}
.event-text { flex:1; }
.event-info { background:rgba(0,0,0,.05); 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); }
.event-info-btn {
width:13px; height:13px; border-radius:50%; border:none;
background:rgba(0,0,0,.15); color:#fff; font-size:.55rem;
cursor:pointer; display:flex; align-items:center; justify-content:center;
flex-shrink:0; font-weight:800;
}
.event-info-btn:hover { background:var(--fjord); }
/* === TUTORIAL === */
.tutorial-overlay {
position:fixed; inset:0; background:rgba(15,30,40,.85);
backdrop-filter:blur(8px); display:flex; align-items:center;
justify-content:center; z-index:50; padding:1.5rem;
}
.tutorial-card {
background:#fff; border-radius:18px; padding:1.6rem 1.9rem;
max-width:520px; box-shadow:0 30px 80px rgba(0,0,0,.4);
}
.tutorial-card h3 { font-size:1.25rem; font-weight:800; margin-bottom:.6rem; color:var(--fjord); }
.tutorial-card p { font-size:.9rem; color:var(--text2); white-space:pre-line; margin-bottom:1rem; }
.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:.75rem; background:var(--fjord); color:#fff;
border:none; border-radius:10px; font-weight:700; font-size:.95rem; cursor:pointer;
}
.tut-btn:hover { background:var(--fjord-d); }
/* === 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:#fff; 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; }
.end-goals { display:flex; flex-direction:column; gap:.3rem; margin:.8rem 0 1rem; text-align:left; }
.end-goal { display:flex; align-items:center; gap:.55rem; font-size:.82rem; padding:.35rem .55rem; border-radius:7px; background:#f7f6f3; }
.end-goal.done { color:var(--moss); background:var(--moss-l); }
.end-goal.fail { color:var(--danger); background:var(--coral-l); }
.end-goal-icon { font-weight:900; font-size:.95rem; width:1rem; text-align:center; }
.end-summary strong { color:var(--danger); }
/* === CITIZEN MODAL === */
.citizen-overlay {
position:fixed; inset:0; z-index:80;
background:rgba(15,30,40,.78); backdrop-filter:blur(8px);
display:flex; align-items:center; justify-content:center; padding:1.5rem;
animation:fadein .2s ease-out;
}
@keyframes fadein { from { opacity:0; } to { opacity:1; } }
.citizen-card {
background:#fff; border-radius:18px; padding:1.6rem 1.8rem;
max-width:520px; width:100%; box-shadow:0 30px 80px rgba(0,0,0,.4);
animation:cardpop .25s cubic-bezier(.22,1,.36,1);
}
@keyframes cardpop {
from { transform:scale(.92) translateY(10px); opacity:0; }
to { transform:scale(1) translateY(0); opacity:1; }
}
.citizen-head { display:flex; gap:.9rem; align-items:center; margin-bottom:.9rem; }
.citizen-avatar {
width:56px; height:56px; border-radius:50%;
background:linear-gradient(135deg, var(--fjord-l), #fff);
border:2px solid var(--fjord);
display:flex; align-items:center; justify-content:center;
font-size:2rem; flex-shrink:0;
}
.citizen-title { font-size:1.05rem; font-weight:800; color:var(--text); }
.citizen-sub { font-size:.7rem; color:var(--text3); text-transform:uppercase; letter-spacing:.05em; font-weight:600; }
.citizen-message {
background:#f7f6f3; border-left:3px solid var(--fjord);
padding:.85rem 1rem; border-radius:0 10px 10px 0;
font-size:.92rem; color:var(--text2); font-style:italic; line-height:1.5;
margin-bottom:1.1rem;
}
.citizen-choices { display:flex; flex-direction:column; gap:.5rem; }
.citizen-choice {
text-align:left; background:#f7f6f3; border:1.5px solid rgba(0,0,0,.06);
border-radius:11px; padding:.7rem .9rem; cursor:pointer; font-family:inherit;
transition:all .15s;
}
.citizen-choice:hover {
background:var(--fjord-l); border-color:var(--fjord);
transform:translateX(3px);
}
.citizen-choice-label { font-size:.88rem; font-weight:700; color:var(--text); }
.citizen-choice-desc { font-size:.72rem; color:var(--text3); margin-top:.15rem; }
/* === INFO OVERLAY (Begriffserklärungen für Kinder) === */
.info-overlay {
position:fixed; inset:0; z-index:90;
background:rgba(15,30,40,.72); backdrop-filter:blur(6px);
display:flex; align-items:center; justify-content:center; padding:1.5rem;
}
.info-card {
background:#fff; border-radius:18px; padding:1.6rem 1.9rem;
max-width:520px; box-shadow:0 30px 80px rgba(0,0,0,.4);
}
.info-card h3 { font-size:1.15rem; font-weight:800; margin-bottom:.7rem; color:var(--fjord); }
.info-card p { font-size:.92rem; color:var(--text2); line-height:1.55; margin-bottom:.8rem; }
.info-card .info-hint {
background:var(--fjord-l); border-left:3px solid var(--fjord);
padding:.7rem .9rem; border-radius:0 8px 8px 0; font-size:.82rem;
color:var(--fjord-d); margin-bottom:.9rem;
}
.info-card .tut-btn { margin-top:.3rem; }
/* === RESPONSIVE === */
@media (max-width: 1100px) {
:root { --side-w: 180px; --shop-w: 230px; }
}
@media (max-width: 900px) {
:root { --side-w: 160px; --shop-w: 200px; --graphs-h: 20vh; }
}
@media (max-width: 760px) {
.panel-status, .panel-goals, .panel-shop { display:none; }
.panel-graphs { left: var(--pad); right: var(--pad); }
}
</style>
</head>
<body>
<!-- 3D-Welt füllt den ganzen Bildschirm -->
<div id="canvas-wrap"></div>
<!-- TOP BAR mit allen Steuerelementen -->
<nav class="floating floating-strong topbar">
<a href="index.html" class="brand">
<img src="assets/img/logo.png" alt="">
Klimawächter
<span class="badge-3d">3D</span>
</a>
<div class="topbar-controls">
<!-- Speed Controls -->
<div class="ctrl-group" title="Simulationsgeschwindigkeit">
<button class="icon-btn" data-speed="0" title="Pause"></button>
<button class="icon-btn active" data-speed="1" title="Normal (×1)">×1</button>
<button class="icon-btn" data-speed="2" title="Schnell (×2)">×2</button>
<button class="icon-btn" data-speed="4" title="Sehr schnell (×4)">×4</button>
</div>
<!-- Kamera Controls -->
<div class="ctrl-group" title="Kamera">
<button class="icon-btn" id="cam-zoom-in" title="Hineinzoomen"></button>
<button class="icon-btn" id="cam-zoom-out" title="Herauszoomen"></button>
<button class="icon-btn" id="cam-center" title="Ansicht zentrieren"></button>
</div>
<div class="topbar-sep"></div>
<!-- Schwierigkeitsgrad-Badge -->
<button class="level-badge" id="level-badge" title="Schwierigkeitsgrad ändern">
<span class="lb-emoji">🟡</span><span class="lb-text">L2</span>
</button>
<div class="topbar-sep"></div>
<!-- System Buttons — nur Symbole -->
<button class="icon-btn" id="btn-save" title="Stand speichern">💾</button>
<button class="icon-btn" id="btn-load" title="Stand laden">📂</button>
<button class="icon-btn" id="btn-finish" title="Simulation beenden"></button>
<button class="icon-btn" id="btn-reset" title="Zurücksetzen">🔄</button>
<a class="icon-btn" href="index.html" title="Zurück zur Übersicht">🏠</a>
</div>
</nav>
<!-- LINKS: Status + Ziele — kompakt -->
<aside class="floating panel-status">
<div class="panel-h">📊 Status</div>
<div id="resources"></div>
<div class="panel-h panel-h-sub">🎯 Ziele</div>
<div id="status-goals"></div>
</aside>
<!-- LINKS DANEBEN: Ereignisse-Strip (sitzt direkt rechts vom Status-Panel,
ist standardmäßig nur eine Card hoch und zeigt das letzte Ereignis.
Klick → klappt nach oben auf, alle Ereignisse scrollbar.
Erneuter Klick (oder außerhalb) → schließt wieder. -->
<aside class="floating panel-events-strip" id="events-strip">
<div class="events-strip-header">
<span class="esh-title">📰 Ereignisse</span>
<span class="esh-toggle"></span>
</div>
<div id="events" class="events-strip-list"></div>
</aside>
<!-- RECHTS: Maßnahmen — über die volle Höhe, breiter -->
<aside class="floating panel-shop">
<div class="panel-h">🏗 Maßnahmen</div>
<div id="measures"></div>
</aside>
<!-- UNTEN MITTE: Graphen (doppelt so groß wie vorher) -->
<div class="floating panel-graphs">
<div class="graphs-row">
<div class="graph-card">
<div class="graph-title">
<span>🌫 CO₂ <span class="unit-chip" data-tip="ppm">ppm <span class="unit-i">i</span></span></span>
<button class="graph-info-btn" data-info="co2" title="Was ist CO₂?">i</button>
</div>
<svg class="graph-svg" id="graph-co2"></svg>
</div>
<div class="graph-card">
<div class="graph-title">
<span>🌡 Temperatur
<span class="unit-chip" data-tip="celsius">°C <span class="unit-i">i</span></span>
<span class="unit-chip" data-tip="paris_goal">Paris <span class="unit-i">i</span></span>
</span>
<button class="graph-info-btn" data-info="temperature" title="Was bedeutet das?">i</button>
</div>
<svg class="graph-svg" id="graph-temp"></svg>
</div>
<div class="graph-card">
<div class="graph-title">
<span>💰 Budget <span class="unit-chip" data-tip="mio_euro">Mio € <span class="unit-i">i</span></span></span>
<button class="graph-info-btn" data-info="budget" title="Was ist das Budget?">i</button>
</div>
<svg class="graph-svg" id="graph-budget"></svg>
</div>
<div class="graph-card">
<div class="graph-title">
<span>🌊 Meer <span class="unit-chip" data-tip="cm">cm <span class="unit-i">i</span></span></span>
<button class="graph-info-btn" data-info="sealevel" title="Meeresspiegelanstieg">i</button>
</div>
<svg class="graph-svg" id="graph-sea"></svg>
</div>
</div>
</div>
<!-- TUTORIAL OVERLAY -->
<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 id="end-screen-host"></div>
<div id="citizen-event-host"></div>
<div id="info-overlay-host"></div>
<div class="graph-zoom-backdrop" id="graph-zoom-backdrop"></div>
<!-- Baumodus-Hinweis: erscheint mittig oben, sobald der Baueditor aktiv ist -->
<div class="build-banner" id="build-banner">
<span class="bb-emoji" id="bb-emoji">🏗</span>
<div class="bb-text">
<div><span id="bb-name" class="bb-name">Maßnahme</span> platzieren</div>
<div class="bb-hint">Klick = bauen · weitere Klicks = Mehrfach-Bau · <kbd>ESC</kbd> oder Rechtsklick = beenden · <kbd>← →</kbd> drehen</div>
</div>
</div>
<!-- Difficulty Selector — wird nur beim ERSTEN Start oder bei Klick aufs Level-Badge angezeigt -->
<div class="difficulty-overlay" id="difficulty-overlay" style="display:none">
<div class="difficulty-card">
<h2>Wähle deinen Schwierigkeitsgrad</h2>
<p class="subtitle">Du kannst das später jederzeit oben in der Leiste ändern.</p>
<div class="difficulty-buttons">
<button class="difficulty-btn lvl-1" data-level="1">
<div class="difficulty-emoji">🟢</div>
<div class="difficulty-name">Lernen</div>
<div class="difficulty-desc">Sanfter Einstieg. Viel Geld, milde Folgen, lange Schonzeit.</div>
<div class="difficulty-stats">950 Mio € Start · 174 €/J</div>
</button>
<button class="difficulty-btn lvl-2" data-level="2">
<div class="difficulty-emoji">🟡</div>
<div class="difficulty-name">Üben</div>
<div class="difficulty-desc">Strategie nötig, du musst wirklich nachdenken.</div>
<div class="difficulty-stats">480 Mio € Start · 93 €/J</div>
</button>
<button class="difficulty-btn lvl-3" data-level="3">
<div class="difficulty-emoji">🔴</div>
<div class="difficulty-name">Profi</div>
<div class="difficulty-desc">Hart. Kein Spielraum, schnelle Konsequenzen.</div>
<div class="difficulty-stats">350 Mio € Start · 78 €/J</div>
</button>
</div>
</div>
</div>
<script type="module">
import { KlimawaechterGame, MEASURES, DIFFICULTY_LEVELS } from './src/sims/sim-05-treibhaus/game.ts'
import { KlimawaechterRenderer3D } from './src/sims/sim-05-treibhaus-3d/game-renderer-3d.ts'
import { GameUI } from './src/ui/game-ui.ts'
import { INFO_TOPICS, openInfoOverlay, installTooltips } from './src/ui/info-overlay.ts'
// Globale Tooltip-Listener für alle data-tip="…" Elemente
installTooltips()
// === Schwierigkeitsgrad bestimmen ===
// Reihenfolge: URL-Parameter > Save-Datei > localStorage > Selector zeigen
const LEVEL_KEY = 'klimawaechter-3d-difficulty'
const DIFFICULTY_OVERLAY = document.getElementById('difficulty-overlay')
function readStoredLevel() {
const url = parseInt(new URLSearchParams(location.search).get('level') || '0', 10)
if (url === 1 || url === 2 || url === 3) return url
// Save-Datei berücksichtigen — wenn ein Save existiert, dessen Level übernehmen
try {
const raw = localStorage.getItem('klimawaechter-3d-save')
if (raw) {
const parsed = JSON.parse(raw)
const lvl = parsed?.subclass?.difficulty
if (lvl === 1 || lvl === 2 || lvl === 3) return lvl
}
} catch {}
const stored = parseInt(localStorage.getItem(LEVEL_KEY) || '0', 10)
if (stored === 1 || stored === 2 || stored === 3) return stored
return 0
}
function showSelector() {
DIFFICULTY_OVERLAY.style.display = 'flex'
}
function hideSelector() {
DIFFICULTY_OVERLAY.style.display = 'none'
}
// Wenn Spieler im Selector ein Level wählt → speichern und Seite neu laden
DIFFICULTY_OVERLAY.querySelectorAll('.difficulty-btn').forEach(btn => {
btn.addEventListener('click', () => {
const lvl = parseInt(btn.getAttribute('data-level'), 10)
if (lvl === 1 || lvl === 2 || lvl === 3) {
localStorage.setItem(LEVEL_KEY, String(lvl))
// Bei Level-Wechsel altes Save löschen, damit ein neues Spiel startet
localStorage.removeItem('klimawaechter-3d-save')
location.reload()
}
})
})
let currentLevel = readStoredLevel()
if (currentLevel === 0) {
// Erstes Spiel — Selector zeigen, Spiel-Init wartet auf Klick (löst reload aus)
showSelector()
// Defensive: Wenn der User die Seite einfach schließt, kein Spiel starten
throw new Error('Waiting for difficulty selection')
}
const game = new KlimawaechterGame(currentLevel)
// Maßnahmen pro Schwierigkeitsgrad filtern.
// Level 1: nur die Kern-Maßnahmen (siehe DIFFICULTY_LEVELS[1].availableMeasures)
// Level 2: Mid-Tier ohne die fragwürdigsten Optionen
// Level 3: alle
const allowedMeasures = DIFFICULTY_LEVELS[currentLevel]?.availableMeasures
const filteredMeasures = allowedMeasures
? MEASURES.filter(m => allowedMeasures.includes(m.id))
: MEASURES
const shopItems = filteredMeasures.map(m => {
const badges = []
if (m.co2Reduction > 0) badges.push({ label: `${m.co2Reduction} CO₂/J`, type: 'reduction' })
if (m.co2Reduction < 0) badges.push({ label: `+${(-m.co2Reduction).toFixed(2)} CO₂/J`, type: 'quality' })
if (m.protection > 0) badges.push({ label: `🛡 +${m.protection} cm`, type: 'protection' })
if (m.powerOutput) badges.push({ label: `⚡ +${m.powerOutput} MW`, type: 'capacity' })
if (m.id === 'sand-fill') badges.push({ label: '⏳ erodiert', type: 'neutral' })
return {
id: m.id,
name: m.name,
emoji: m.emoji,
description: m.description,
cost: m.cost,
upkeep: m.upkeep,
badges,
}
})
const ui = new GameUI({
game,
Renderer: KlimawaechterRenderer3D,
shopItems,
onBuy: (id) => game.buyMeasure(id),
onDemolish: (id) => game.demolishMeasure(id),
getOwnedCount: (id) => game.getMeasureCount(id),
// Baueditor: Klick auf Maßnahme → Placement-Mode im Renderer starten,
// statt sofort zu kaufen. Wenn der Renderer ein Placement bestätigt,
// ruft er den onConfirm-Callback mit der gewählten Position auf.
onStartPlacement: (id) => {
const r = ui.getRenderer()
if (!r || typeof r.startPlacement !== 'function') {
// Fallback: Auto-Platzierung
game.buyMeasure(id)
return
}
// Vorab-Check: Budget reicht?
const measure = MEASURES.find(m => m.id === id)
if (!measure) return
if (game.getResource('budget') < measure.cost) {
// game.buyMeasure() würde das Warning-Event setzen — wir nutzen es als Trigger
game.buyMeasure(id)
return
}
// Placement-Mode → Simulation pausieren bis bestätigt/abgebrochen
pauseGameForOverlay('placement-mode')
// Baumodus-Banner einblenden
const banner = document.getElementById('build-banner')
const bbEmoji = document.getElementById('bb-emoji')
const bbName = document.getElementById('bb-name')
if (banner && bbEmoji && bbName) {
bbEmoji.textContent = measure.emoji
bbName.textContent = measure.name
banner.classList.add('visible')
}
r.startPlacement(
id,
(mid, x, z) => {
// Confirmed: kaufen mit Position. Mehrfach-Bau: Mode bleibt aktiv.
game.buyMeasure(mid, { x, z })
const m = MEASURES.find(x => x.id === mid)
if (!m || game.getResource('budget') < m.cost) {
r.cancelPlacement()
}
},
() => {
// Cancelled (ESC, Rechtsklick, oder Budget aufgebraucht)
if (banner) banner.classList.remove('visible')
resumeGameForOverlay('placement-mode')
},
)
},
graphs: [
{
id: 'graph-co2', title: 'CO₂', field: 'co2',
yMin: 380, yMax: 700, color: '#8a7868', unit: 'ppm',
zones: [
{ from: 380, to: 450, color: 'rgba(140,200,140,.18)', label: 'Klimaziel' },
{ from: 450, to: 550, color: 'rgba(220,190,120,.18)', label: 'Kritisch' },
{ from: 550, to: 700, color: 'rgba(200,100,100,.18)', label: 'Gefahr' },
],
lines: [
{ at: 450, color: 'rgba(100,60,40,.6)', label: 'Ziel', dashed: true },
],
},
{
id: 'graph-temp', title: 'Temperatur', field: 'temperature',
yMin: 15, yMax: 19, color: '#c07a6b', unit: '°C',
zones: [
{ from: 15, to: 16, color: 'rgba(140,200,140,.18)', label: 'sicher' },
{ from: 16, to: 17, color: 'rgba(220,190,120,.18)', label: '+1°C' },
{ from: 17, to: 18, color: 'rgba(220,140,100,.20)', label: '+2°C (Pariser Ziel)' },
{ from: 18, to: 19, color: 'rgba(200,80,60,.20)', label: 'Gefahr' },
],
lines: [
{ at: 17, color: '#b04a3a', label: 'Klimaziel 17°C', dashed: false },
],
},
{
id: 'graph-budget', title: 'Budget', field: 'budget',
yMin: -200, yMax: 1000, color: '#5a8a5e', unit: 'Mio €',
autoScale: true, // wandert mit, wenn mehr Geld verdient wird
zones: [
{ from: -200, to: 0, color: 'rgba(200,100,100,.18)', label: 'Pleite' },
],
lines: [
{ at: 0, color: '#b04a3a', label: '0', dashed: true },
],
},
{
id: 'graph-sea', title: 'Meeresspiegel', field: 'sealevel',
yMin: 0, yMax: 120, color: '#4a7c8a', unit: 'cm',
zones: [
{ from: 0, to: 30, color: 'rgba(140,200,140,.15)', label: 'Strand ok' },
{ from: 30, to: 60, color: 'rgba(220,190,120,.18)', label: 'Strand weg' },
{ from: 60, to: 100,color: 'rgba(220,140,100,.20)', label: 'Häuser!' },
{ from: 100,to: 120,color: 'rgba(200,80,60,.22)', label: 'Katastrophe' },
],
lines: [
{ at: 30, color: '#c4a35a', label: 'Strand', dashed: true },
{ at: 60, color: '#c07a6b', label: 'Häuser', dashed: true },
],
},
],
saveKey: 'klimawaechter-3d-save',
yearOffset: 2025,
// Status-Box zeigt nur noch Werte, die NICHT im Graph stehen
// (CO₂, Temperatur, Meer sind unten in den Graph-Pills sichtbar)
statusResources: ['budget', 'population', 'power'],
})
// === Kamera-Steuerung anbinden ===
const callRenderer = (method) => {
const r = ui.getRenderer()
if (r && typeof r[method] === 'function') r[method]()
}
document.getElementById('cam-zoom-in') ?.addEventListener('click', () => callRenderer('zoomIn'))
document.getElementById('cam-zoom-out')?.addEventListener('click', () => callRenderer('zoomOut'))
document.getElementById('cam-center') ?.addEventListener('click', () => callRenderer('recenter'))
// === Info-Buttons an Graphen anbinden ===
document.querySelectorAll('.graph-info-btn').forEach(btn => {
btn.addEventListener('click', () => {
const key = btn.getAttribute('data-info')
if (key && INFO_TOPICS[key]) openInfoOverlay(INFO_TOPICS[key])
})
})
// === Auto-Pause bei offenen Overlays ===
// Token-basiertes Pause-System: jeder Overlay-Typ registriert sich mit
// einem eindeutigen Schlüssel; solange mindestens ein Token aktiv ist,
// bleibt das Spiel bei Speed 0 stehen. Beim letzten Schließen wird die
// vorherige Geschwindigkeit wiederhergestellt.
const overlayPauseTokens = new Set()
let speedBeforeOverlayPause = 1
function pauseGameForOverlay(token) {
if (overlayPauseTokens.size === 0) {
// Aktuelle Geschwindigkeit merken (außer es ist eh schon pausiert)
const cur = game.getSnapshot().speed
if (cur !== 0) speedBeforeOverlayPause = cur
game.setSpeed(0)
// Speed-Buttons im UI synchronisieren
document.querySelectorAll('[data-speed]').forEach(b => b.classList.remove('active'))
document.querySelector('[data-speed="0"]')?.classList.add('active')
}
overlayPauseTokens.add(token)
}
function resumeGameForOverlay(token) {
overlayPauseTokens.delete(token)
if (overlayPauseTokens.size === 0) {
const restore = speedBeforeOverlayPause || 1
game.setSpeed(restore)
document.querySelectorAll('[data-speed]').forEach(b => b.classList.remove('active'))
document.querySelector(`[data-speed="${restore}"]`)?.classList.add('active')
}
}
// Citizen-Modal und Info-Overlay erscheinen/verschwinden ohne JS-API.
// Mit einem MutationObserver lauschen wir auf das DOM und pausieren das
// Spiel automatisch, sobald ein solches Modal sichtbar wird.
function observeOverlayForPause(selector, token) {
let isOpen = false
const check = () => {
const el = document.querySelector(selector)
const visible = !!el && getComputedStyle(el).display !== 'none'
if (visible && !isOpen) { isOpen = true; pauseGameForOverlay(token) }
else if (!visible && isOpen) { isOpen = false; resumeGameForOverlay(token) }
}
const mo = new MutationObserver(check)
mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] })
return mo
}
observeOverlayForPause('.citizen-overlay', 'citizen-event')
observeOverlayForPause('.info-overlay', 'info-overlay')
// === Graph-Zoom: Click-to-zoom mit FLIP-Animation ===
// Klick auf eine Graph-Card → fliegt zentriert in die Mitte (groß).
// Erneuter Klick (oder Backdrop-Klick) → fliegt zurück.
//
// WICHTIG: .panel-graphs hat `backdrop-filter`, das macht es zum
// "containing block" für position:fixed Kinder. Wir hängen die Karte
// deshalb beim Zoom temporär ans <body> um (mit Platzhalter im Grid),
// damit position:fixed wirklich relativ zum Viewport funktioniert.
const zoomBackdrop = document.getElementById('graph-zoom-backdrop')
let zoomedCard = null
let zoomOriginalRect = null
let zoomOriginalParent = null
let zoomNextSibling = null
let zoomPlaceholder = null
function zoomGraph(card) {
if (zoomedCard) return
const rect = card.getBoundingClientRect()
zoomOriginalRect = rect
zoomedCard = card
zoomOriginalParent = card.parentNode
zoomNextSibling = card.nextSibling
// Platzhalter im Grid behalten, damit das Layout der anderen Karten stabil bleibt
zoomPlaceholder = document.createElement('div')
zoomPlaceholder.className = 'graph-card-placeholder'
zoomPlaceholder.style.minWidth = '0'
zoomPlaceholder.style.minHeight = '0'
zoomOriginalParent.insertBefore(zoomPlaceholder, card)
// Karte ans <body> umhängen — escape backdrop-filter containing block
document.body.appendChild(card)
// Schritt 1: Karte fixiert an aktueller Position (visuell unverändert)
card.style.position = 'fixed'
card.style.left = rect.left + 'px'
card.style.top = rect.top + 'px'
card.style.width = rect.width + 'px'
card.style.height = rect.height + 'px'
card.style.margin = '0'
card.style.zIndex = 160
// Reflow erzwingen
void card.offsetWidth
// Schritt 2: Transition aktivieren und Zielposition setzen
card.style.transition = 'all .42s cubic-bezier(.22,1,.36,1)'
const targetW = Math.min(940, window.innerWidth * 0.78)
const targetH = Math.min(560, window.innerHeight * 0.72)
card.style.left = ((window.innerWidth - targetW) / 2) + 'px'
card.style.top = ((window.innerHeight - targetH) / 2) + 'px'
card.style.width = targetW + 'px'
card.style.height = targetH + 'px'
card.classList.add('zoomed')
zoomBackdrop.classList.add('active')
// Nach Animation-Ende: Graph neu zeichnen für die größere SVG-Fläche
const onEnd = () => {
card.removeEventListener('transitionend', onEnd)
if (typeof ui.redrawGraphs === 'function') ui.redrawGraphs()
}
card.addEventListener('transitionend', onEnd)
// Spiel pausieren während Zoom
pauseGameForOverlay('graph-zoom')
}
function unzoomGraph() {
if (!zoomedCard || !zoomOriginalRect) return
const card = zoomedCard
const rect = zoomOriginalRect
// Animation zurück zur Originalposition
card.style.left = rect.left + 'px'
card.style.top = rect.top + 'px'
card.style.width = rect.width + 'px'
card.style.height = rect.height + 'px'
card.classList.remove('zoomed')
zoomBackdrop.classList.remove('active')
const cleanup = () => {
card.removeEventListener('transitionend', cleanup)
// Karte zurück in den Original-Container an die richtige Stelle setzen
if (zoomPlaceholder && zoomPlaceholder.parentNode) {
zoomPlaceholder.parentNode.insertBefore(card, zoomPlaceholder)
zoomPlaceholder.remove()
} else if (zoomOriginalParent) {
if (zoomNextSibling && zoomNextSibling.parentNode === zoomOriginalParent) {
zoomOriginalParent.insertBefore(card, zoomNextSibling)
} else {
zoomOriginalParent.appendChild(card)
}
}
// Inline-Styles aufräumen
card.style.position = ''
card.style.left = ''
card.style.top = ''
card.style.width = ''
card.style.height = ''
card.style.margin = ''
card.style.zIndex = ''
card.style.transition = ''
if (typeof ui.redrawGraphs === 'function') ui.redrawGraphs()
zoomPlaceholder = null
zoomOriginalParent = null
zoomNextSibling = null
}
card.addEventListener('transitionend', cleanup)
zoomedCard = null
zoomOriginalRect = null
// Spiel wieder fortsetzen
resumeGameForOverlay('graph-zoom')
}
// Klick-Handler an alle Graph-Karten anhängen
document.querySelectorAll('.graph-card').forEach(card => {
card.addEventListener('click', (ev) => {
// Klick auf den Info-Button (i) NICHT als Zoom werten
if (ev.target.closest('.graph-info-btn')) return
// Klick auf einen Unit-Chip (für Tooltips) NICHT als Zoom werten
if (ev.target.closest('.unit-chip')) return
if (zoomedCard === card) {
unzoomGraph()
} else if (!zoomedCard) {
zoomGraph(card)
}
})
})
zoomBackdrop.addEventListener('click', unzoomGraph)
// === Events-Strip: Click toggles open/closed ===
// Default: nur die neueste Card sichtbar (max-height ~84px).
// Klick auf den Strip: max-height wächst, alle Ereignisse scrollbar.
// Klick außerhalb: schließt wieder. Während offen: Simulation pausiert.
const eventsStrip = document.getElementById('events-strip')
function openEventsStrip() {
if (!eventsStrip || eventsStrip.classList.contains('open')) return
eventsStrip.classList.add('open')
pauseGameForOverlay('events-strip')
}
function closeEventsStrip() {
if (!eventsStrip || !eventsStrip.classList.contains('open')) return
eventsStrip.classList.remove('open')
resumeGameForOverlay('events-strip')
}
if (eventsStrip) {
// Wir benutzen die capture-phase, damit der Strip-Toggle IMMER zuerst läuft
// (vor dem document-outside-click handler) und stoppen die Propagation,
// damit der document handler den Strip-Klick nie als "outside" interpretiert.
eventsStrip.addEventListener('click', (ev) => {
// Klick auf einen Info-Button im offenen Zustand NICHT als Toggle werten
if (eventsStrip.classList.contains('open') && ev.target.closest('.event-info-btn')) return
ev.stopPropagation()
if (eventsStrip.classList.contains('open')) closeEventsStrip()
else openEventsStrip()
})
// Klick außerhalb des Strips schließt ihn (im offenen Zustand)
document.addEventListener('click', (ev) => {
if (!eventsStrip.classList.contains('open')) return
if (eventsStrip.contains(ev.target)) return
closeEventsStrip()
})
}
// === Level-Badge in der Topbar aktualisieren + klickbar machen ===
const lvlConfig = DIFFICULTY_LEVELS[currentLevel]
const badge = document.getElementById('level-badge')
if (badge && lvlConfig) {
badge.querySelector('.lb-emoji').textContent = lvlConfig.emoji
badge.querySelector('.lb-text').textContent = `L${lvlConfig.id} · ${lvlConfig.label}`
badge.title = `Schwierigkeit: ${lvlConfig.label} — klicken, um zu wechseln (startet neu)`
badge.addEventListener('click', () => {
if (confirm('Schwierigkeit wechseln? Deine aktuelle Simulation geht verloren.')) {
showSelector()
}
})
}
</script>
</body>
</html>