Files
geograsim/App/sims/energiemanager/game.html
T
Adminator 7de71b0e83 Energiemanager: Blackouts fuer Lehrer sichtbar machen
- game.html: bei Blackout sofort action:'blackout' an /progress feuern
  (setId/Tag/Block/Frequenz/Ursache, score:0) — Fehlversuch bleibt auch
  bei Set-Abbruch sichtbar, kein player_progress/Sterne.
- progress.php: neuer action:'blackout' -> eigene assessments-Zeile,
  KEIN XP/plays/best_stars.
- live.php: Blackout-Flag (event:'blackout') durch die Tag-Summary reichen
  (blackout/blackoutCount pro Session).
- teacher.html: needsHelp energiemanager (>=2 Blackouts oder Tag <4/8);
  Blackout-Spalte + '🔌 neu planen'-Status in Aktuell; Tag-Balken als
  🔌 Blackout kennzeichnen; Run-Karte: eigenes Blackout-Kaertchen +
  '🔌 N Blackouts'-Badge im fertigen Set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 13:19:21 +02:00

2864 lines
128 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, viewport-fit=cover">
<title>Energiemanager — Tag planen</title>
<link rel="icon" type="image/png" href="../../favicon-96x96.png" sizes="96x96">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/design-system.css">
<style>
/* ===== Energiemanager — Stufe 1 (2 Seen + 2 Pumpen + step-by-step) ===== */
html, body { height: 100%; margin: 0; padding: 0;
font-family: 'Inter', system-ui, sans-serif;
background: var(--ggs-bg); color: var(--ggs-text);
touch-action: manipulation; }
.em-layout { display: grid; grid-template-rows: auto 1fr;
height: 100dvh; }
.em-main {
padding: 14px 18px 22px;
max-width: 1100px; margin: 0 auto; width: 100%;
box-sizing: border-box; overflow-y: auto;
}
/* Mute-Toggle im Header */
.ggs-btn-ghost.muted { opacity: 0.55; }
/* Set-Tabs (oben) — sechs Sets als kleinere Pillen */
.em-set-row { margin-bottom: 8px; }
.em-set-tabs { display: flex; gap: 5px; flex-wrap: wrap; }
.em-set-tab {
-webkit-appearance: none; appearance: none;
background: var(--ggs-white, #fff); color: var(--ggs-text-muted);
border: 1px solid var(--ggs-border, #e2ddd1);
border-radius: 999px; padding: 5px 11px;
font-size: 0.78rem; font-weight: 600;
cursor: pointer; font-family: inherit;
min-height: 32px;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.em-set-tab:hover { background: var(--ggs-fjord-light); }
.em-set-tab.active {
background: var(--ggs-fjord-dark); color: #fff;
border-color: var(--ggs-fjord-dark);
}
.em-day-head {
display: flex; align-items: center;
justify-content: space-between; gap: 12px;
margin-bottom: 8px; flex-wrap: wrap;
}
.em-day-tabs { display: flex; gap: 6px; }
.em-day-tab {
-webkit-appearance: none; appearance: none;
background: var(--ggs-white, #fff);
color: var(--ggs-text-muted);
border: 1.5px solid var(--ggs-border, #e2ddd1);
border-radius: 999px;
padding: 7px 14px;
font-size: 0.86rem; font-weight: 700;
cursor: pointer; font-family: inherit;
min-height: 36px;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.em-day-tab:hover { background: var(--ggs-fjord-light); }
.em-day-tab.active {
background: var(--ggs-fjord); color: #fff;
border-color: var(--ggs-fjord-dark);
}
.em-day-tab:disabled {
opacity: 0.5; cursor: not-allowed;
background: var(--ggs-white);
}
.em-day-head .em-block-running {
background: #fff8d6; border: 1px solid #d6b94e;
color: #5e5320; padding: 4px 10px;
border-radius: 999px; font-size: 0.8rem;
font-weight: 700;
}
/* ============ Querschnitt (oben) ============ */
.em-cross {
background: var(--ggs-white, #fff);
border-radius: var(--ggs-radius-md, 8px);
box-shadow: var(--ggs-shadow-sm);
overflow: hidden; margin-bottom: 12px;
position: relative;
}
.em-cross-svg {
width: 100%; height: 280px; display: block;
background: linear-gradient(180deg, #d6e7ee 0%, #e8eef0 65%, #f0ebde 100%);
}
@media (max-width: 700px) {
.em-cross-svg { height: 220px; }
}
/* SVG-Klassen für den Querschnitt */
.em-water { fill: url(#emWaterGrad);
transition: y 1.4s ease, height 1.4s ease; }
.em-pipe-shell { stroke: #9E9E9E; stroke-width: 16;
stroke-linejoin: round; stroke-linecap: round; fill: none; }
.em-pipe-flow {
stroke-width: 3; fill: none;
stroke-linecap: round;
stroke-dasharray: 12 8;
stroke-dashoffset: 0;
opacity: 0;
transition: opacity 0.35s ease;
}
.em-pipe-flow.active.turbine {
stroke: #01579B; opacity: 1;
animation: em-flow-fwd var(--em-flow-dur, 1.2s) linear infinite;
}
.em-pipe-flow.active.pump {
stroke: #00C853; opacity: 1;
animation: em-flow-bwd var(--em-flow-dur, 1.2s) linear infinite;
}
@keyframes em-flow-fwd { to { stroke-dashoffset: -20; } }
@keyframes em-flow-bwd { to { stroke-dashoffset: 20; } }
.em-cs-label {
font-family: 'Inter', system-ui, sans-serif;
paint-order: stroke;
stroke: rgba(255,255,255,.85);
stroke-width: 4px;
stroke-linejoin: round;
}
/* ============ Bedarf/Solar-Bargraph ============ */
.em-graph-zone {
background: var(--ggs-white, #fff);
border-radius: var(--ggs-radius-md, 8px);
box-shadow: var(--ggs-shadow-sm);
padding: 12px 14px 8px; margin-bottom: 12px;
}
.em-graph-svg { width: 100%; height: 140px; display: block; }
.em-legend {
display: flex; gap: 14px; padding: 4px 4px 0;
font-size: 0.78rem; color: var(--ggs-text-muted);
flex-wrap: wrap;
}
.em-legend-row { display: flex; align-items: center; gap: 5px; }
.em-legend-swatch {
display: inline-block; width: 14px; height: 4px; border-radius: 2px;
}
.em-legend-swatch.demand { background: #c85c4a; }
.em-legend-swatch.solar { background: #e8a82e; }
/* ============ Plan-Grid: 8 Block-Spalten mit 5-Stufen-Slidern ============ */
.em-plan {
background: var(--ggs-white, #fff);
border-radius: var(--ggs-radius-md, 8px);
box-shadow: var(--ggs-shadow-sm);
padding: 14px;
}
.em-plan h3 {
margin: 0 0 8px; font-size: 0.95rem;
color: var(--ggs-fjord-dark);
}
.em-plan-help { font-size: 0.82rem; color: var(--ggs-text-muted);
margin: 0 0 12px; line-height: 1.5; }
.em-plan-help b { color: var(--ggs-text); }
.em-blocks {
display: grid; grid-template-columns: repeat(8, 1fr);
gap: 8px;
}
@media (max-width: 760px) {
.em-blocks { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 420px) {
.em-blocks { grid-template-columns: repeat(2, 1fr); }
}
.em-block {
display: flex; flex-direction: column; gap: 6px;
padding: 8px 6px; border-radius: 7px;
background: #fafaf6; border: 1px solid var(--ggs-border, #e2ddd1);
min-width: 0;
transition: border-color 0.2s, box-shadow 0.2s;
}
.em-block.running {
border-color: #d6b94e; background: #fff8d6;
box-shadow: 0 0 0 2px rgba(214,185,78,.4);
}
.em-block.done { opacity: 0.85; }
.em-block-label {
text-align: center; font-size: 0.74rem;
color: var(--ggs-text-muted); font-weight: 700;
font-variant-numeric: tabular-nums;
}
.em-block-stats {
display: flex; justify-content: space-around;
font-size: 0.72rem;
margin-bottom: 2px;
}
.em-block-stats .demand { color: #c85c4a; font-weight: 700; }
.em-block-stats .solar { color: #c89518; font-weight: 700; }
/* 5-Stufen-Slider — vertikal in der Block-Spalte (eng auf Mobile, üppig auf Desktop) */
.em-stage-slider {
-webkit-appearance: none; appearance: none;
width: 100%; height: 30px;
background: transparent; cursor: pointer;
touch-action: manipulation; margin: 0; padding: 0;
}
.em-stage-slider:focus { outline: none; }
.em-stage-slider::-webkit-slider-runnable-track {
height: 6px; border-radius: 3px;
background: linear-gradient(90deg,
#2E7D32 0%, #2E7D32 25%,
#ECEFF1 25%, #ECEFF1 75%,
#01579B 75%, #01579B 100%);
}
.em-stage-slider::-moz-range-track {
height: 6px; border-radius: 3px; background: #ECEFF1;
}
.em-stage-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 28px; height: 28px; border-radius: 14px;
background: #fff; border: 3px solid #455A64;
margin-top: -11px;
box-shadow: 0 2px 5px rgba(0,0,0,.25);
cursor: grab;
}
.em-stage-slider::-moz-range-thumb {
width: 24px; height: 24px; border-radius: 12px;
background: #fff; border: 3px solid #455A64;
box-shadow: 0 2px 5px rgba(0,0,0,.25);
}
.em-stage-slider[disabled] { opacity: 0.6; cursor: not-allowed; }
.em-stage-readout {
text-align: center; font-size: 0.78rem;
font-weight: 700; line-height: 1.25;
padding: 4px 2px;
border-radius: 5px;
background: #f0ece3;
color: var(--ggs-text);
min-height: 38px;
display: flex; align-items: center; justify-content: center;
}
.em-stage-readout.pump-2 { background: #b8d8b8; color: #1B5E20; }
.em-stage-readout.pump-1 { background: #d6e8d6; color: #2E7D32; }
.em-stage-readout.off { background: #ECEFF1; color: #455A64; }
.em-stage-readout.turb-1 { background: #d8e4ec; color: #01579B; }
.em-stage-readout.turb-2 { background: #b8cee0; color: #013A66; }
.em-block-feedback {
text-align: center; font-size: 0.76rem;
margin-top: 2px; padding: 4px 2px;
border-radius: 5px; font-weight: 700;
line-height: 1.2;
}
.em-block-feedback.ok { background: #d6e8d6; color: #2E7D32; }
.em-block-feedback.warn { background: #fff3d6; color: #7a5a1a; }
.em-block-feedback.bad { background: #f3d0d0; color: #b32d2d; }
/* Run-Buttons + Bilanz */
.em-actions-row {
display: flex; justify-content: center;
gap: 12px; margin-top: 14px; flex-wrap: wrap;
}
.em-btn-primary {
background: var(--ggs-fjord); color: #fff;
border: none; border-radius: 8px;
padding: 12px 28px; font-size: 1rem; font-weight: 700;
cursor: pointer; font-family: inherit;
min-height: 44px;
}
.em-btn-primary:hover { background: var(--ggs-fjord-dark); }
.em-btn-primary:disabled { background: #B0BEC5; cursor: not-allowed; }
.em-btn-secondary {
background: var(--ggs-white, #fff); color: var(--ggs-fjord-dark);
border: 1.5px solid var(--ggs-fjord);
border-radius: 8px;
padding: 12px 28px; font-size: 1rem; font-weight: 600;
cursor: pointer; font-family: inherit;
min-height: 44px;
}
.em-btn-secondary:hover { background: var(--ggs-fjord-light); }
.em-result-row {
display: grid; grid-template-columns: 1fr 1fr 1fr;
gap: 10px; margin-top: 14px;
}
@media (max-width: 700px) { .em-result-row { grid-template-columns: 1fr; } }
.em-result-card {
background: var(--ggs-white, #fff);
border-radius: var(--ggs-radius-md, 8px);
box-shadow: var(--ggs-shadow-sm);
padding: 12px 14px; text-align: center;
}
.em-result-card .n {
font-size: 1.6rem; font-weight: 800;
color: var(--ggs-fjord-dark);
font-variant-numeric: tabular-nums;
}
.em-result-card .l {
font-size: 0.78rem; color: var(--ggs-text-muted);
text-transform: uppercase; letter-spacing: 0.04em;
}
.em-result-card.good .n { color: var(--ggs-moss-dark); }
.em-result-card.warn .n { color: #c89518; }
.em-result-card.bad .n { color: var(--ggs-coral); }
.em-resolution-summary {
background: #f5f3ea;
border-left: 4px solid var(--ggs-fjord);
border-radius: 6px;
padding: 12px 16px; margin-top: 14px;
font-size: 0.92rem; line-height: 1.55;
}
.em-resolution-summary h3 {
margin: 0 0 6px; font-size: 1rem;
color: var(--ggs-fjord-dark);
}
/* === Progress-Pill „Block X/8 · Tag Y/3 · Set Z/6" === */
.em-progress-pill {
display: inline-flex; align-items: center; gap: 8px;
background: rgba(74,124,138,0.10);
color: var(--ggs-fjord-dark);
padding: 4px 12px; border-radius: 999px;
font-size: 0.78rem; font-weight: 700;
white-space: nowrap;
}
.em-progress-pill .sep { opacity: 0.4; }
/* === Set-Knoten-Kette: 6 Sets als Prozess-Punkte (über den Set-Tabs) === */
.em-set-flow {
display: flex; gap: 0; align-items: flex-start;
margin: 0 0 12px; padding: 10px 8px;
background: var(--ggs-white, #fff);
border: 1px solid var(--ggs-border, #e2ddd1);
border-radius: var(--ggs-radius-md, 8px);
overflow-x: auto;
}
.em-set-node {
display: flex; flex-direction: column; align-items: center;
flex: 1 1 0; min-width: 78px; padding: 0 4px;
position: relative; text-align: center;
}
.em-set-node + .em-set-node::before {
content: ''; position: absolute;
top: 18px; left: -50%; right: 50%;
height: 2px; background: rgba(31,75,55,0.14); z-index: 0;
}
.em-set-node.done + .em-set-node::before { background: var(--ggs-moss, #5a8a5e); }
.em-set-node-dot {
width: 36px; height: 36px; border-radius: 50%;
background: var(--ggs-beige, #e8e4d8);
border: 2px solid rgba(31,75,55,0.18);
display: flex; align-items: center; justify-content: center;
font-size: 1.05rem; z-index: 1; transition: all 0.3s ease;
}
.em-set-node.done .em-set-node-dot {
background: var(--ggs-moss, #5a8a5e); border-color: var(--ggs-moss, #5a8a5e); color: #fff;
}
.em-set-node.active .em-set-node-dot {
background: #ffd35a; border-color: var(--ggs-fjord-dark);
box-shadow: 0 0 0 4px rgba(255,211,90,0.35);
}
.em-set-node-label {
margin-top: 6px; font-size: 0.66rem; font-weight: 700;
color: var(--ggs-fjord-dark); line-height: 1.15;
}
.em-set-node:not(.done):not(.active) .em-set-node-label { opacity: 0.45; }
.em-set-node-stars {
margin-top: 3px; font-size: 0.66rem; color: #e8c547; letter-spacing: 0.04em;
}
/* === Glossar-Begriffe (Tooltip/Popup) === */
.em-glossar-term {
cursor: help;
border-bottom: 1px dashed rgba(74,124,138,0.5);
text-decoration: none;
color: inherit;
}
.em-glossar-term:hover { border-bottom-color: var(--ggs-fjord-dark); background: rgba(74,124,138,0.06); }
.em-glossar-popup {
position: fixed; z-index: 9999;
background: #fff;
border: 1px solid var(--ggs-border, #e2ddd1);
border-radius: 10px;
box-shadow: 0 8px 30px rgba(20,32,26,0.25);
padding: 12px 14px;
max-width: 320px; min-width: 220px;
font-size: 0.85rem;
line-height: 1.45;
color: var(--ggs-text);
animation: emGlossarPop 0.2s ease-out;
}
@keyframes emGlossarPop {
from { transform: translateY(-4px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.em-glossar-title {
font-weight: 800; color: var(--ggs-fjord-dark);
margin-bottom: 4px; font-size: 0.92rem;
}
.em-glossar-text { color: var(--ggs-text); }
.em-glossar-unit {
margin-top: 6px; font-size: 0.72rem;
color: var(--ggs-text-muted); font-style: italic;
}
.em-glossar-link {
display: inline-block; margin-top: 8px;
color: var(--ggs-fjord-dark); text-decoration: none;
font-weight: 700; font-size: 0.78rem;
}
.em-glossar-link:hover { text-decoration: underline; }
/* === Onboarding-Modal === */
.em-onboard-overlay {
position: fixed; inset: 0;
background: rgba(31,75,55,0.62);
display: flex; align-items: center; justify-content: center;
z-index: 9200; padding: 16px;
backdrop-filter: blur(3px);
}
.em-onboard-overlay[hidden] { display: none; }
.em-onboard-card {
background: #fff; border-radius: 16px;
padding: 22px 26px 18px;
max-width: 560px; width: 100%;
max-height: 92vh; overflow-y: auto;
box-shadow: 0 16px 48px rgba(20,32,26,0.35);
animation: emOnbPop 0.45s cubic-bezier(.18,1.4,.6,1);
}
@keyframes emOnbPop {
from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.em-onboard-dots {
display: flex; gap: 6px; justify-content: center; margin-bottom: 12px;
}
.em-onboard-dots .dot {
width: 9px; height: 9px; border-radius: 50%;
background: rgba(31,75,55,0.16); transition: all 0.25s;
}
.em-onboard-dots .dot.done { background: var(--ggs-moss, #5a8a5e); }
.em-onboard-dots .dot.active { background: var(--ggs-fjord-dark); transform: scale(1.35); }
.em-onboard-title {
margin: 0 0 12px;
color: var(--ggs-fjord-dark);
font-size: 1.3rem; font-weight: 800;
text-align: center;
}
.em-onboard-visual {
background: linear-gradient(180deg, #e3eef2 0%, #f5f3ea 100%);
border-radius: 12px;
margin: 0 -4px 12px;
padding: 12px;
min-height: 180px;
display: flex; align-items: center; justify-content: center;
}
.em-onboard-visual svg { width: 100%; max-width: 460px; height: auto; }
.em-onboard-text {
font-size: 0.94rem; line-height: 1.55;
color: var(--ggs-text); margin-bottom: 18px;
}
.em-onboard-text b { color: var(--ggs-fjord-dark); }
.em-onboard-actions {
display: flex; justify-content: space-between;
gap: 10px; margin-top: 6px; flex-wrap: wrap;
}
/* Demo-Animationen im Onboarding */
@keyframes emOnbPumpUp {
0%, 100% { transform: translateY(0); opacity: 0.85; }
50% { transform: translateY(-46px); opacity: 1; }
}
@keyframes emOnbTurbDown {
0%, 100% { transform: translateY(-46px); opacity: 0.85; }
50% { transform: translateY(0); opacity: 1; }
}
@keyframes emOnbFill {
0% { height: 18px; y: 80; }
50% { height: 52px; y: 46; }
100% { height: 18px; y: 80; }
}
.onb-arrow-pump { animation: emOnbPumpUp 3s ease-in-out infinite; }
.onb-arrow-turbine { animation: emOnbTurbDown 3s ease-in-out infinite; }
.onb-bolt-pulse { animation: emOnbBolt 2.2s ease-in-out infinite; }
@keyframes emOnbBolt {
0%, 100% { opacity: 0.3; transform: scale(0.92); }
50% { opacity: 1; transform: scale(1.08); }
}
/* === Netz-Monitor === */
.em-grid-monitor {
background: var(--ggs-white, #fff);
border: 1px solid var(--ggs-border, #e2ddd1);
border-radius: 8px;
padding: 10px 14px 12px;
margin: 12px 0;
}
.em-gm-head {
display: flex; align-items: center;
justify-content: space-between; margin-bottom: 6px;
flex-wrap: wrap; gap: 6px;
}
.em-gm-title {
font-weight: 700; color: var(--ggs-fjord-dark);
font-size: 0.92rem;
}
.em-gm-sub {
font-weight: 500; color: var(--ggs-text-muted);
font-size: 0.72rem; margin-left: 6px;
}
.em-gm-pill {
background: #dceadd; color: var(--ggs-moss-dark, #356b3a);
padding: 3px 10px; border-radius: 999px;
font-size: 0.74rem; font-weight: 700;
}
.em-gm-pill.warn { background: #fff1c8; color: #a37800; }
.em-gm-pill.bad { background: #fbe6e2; color: var(--ggs-coral, #c07a6b); }
.em-gm-body {
display: block; /* Tachometer ist jetzt im Kraftwerk-Row, hier nur noch der breite Graph */
}
.em-gm-chart {
width: 100%;
min-height: 200px;
}
.em-gm-chart-svg {
width: 100%;
height: clamp(180px, 26vh, 280px);
display: block;
border-radius: 6px;
}
/* === Kraftwerks-Querschnitt + Tachometer-Aside als 2-Spalten-Row === */
.em-cross-row {
display: grid;
grid-template-columns: minmax(0, 1fr) clamp(220px, 22%, 290px);
gap: 14px;
margin-bottom: 12px;
align-items: stretch;
}
@media (max-width: 900px) {
.em-cross-row { grid-template-columns: 1fr; }
}
.em-cross-row .em-cross { margin-bottom: 0; }
.em-cross-row .em-cross-svg { height: 360px; }
@media (max-width: 700px) { .em-cross-row .em-cross-svg { height: 240px; } }
.em-tacho-aside {
background: var(--ggs-white, #fff);
border: 1px solid var(--ggs-border, #e2ddd1);
border-radius: 8px;
padding: 14px 16px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
box-shadow: var(--ggs-shadow-sm);
}
.em-tacho-aside .em-gm-title {
font-weight: 700; color: var(--ggs-fjord-dark);
font-size: 1.0rem; margin-bottom: 4px;
}
.em-tacho-aside .em-gm-sub {
font-weight: 500; color: var(--ggs-text-muted);
font-size: 0.74rem; margin-bottom: 10px;
}
.em-tacho-aside .em-gm-tacho-svg {
width: 100%; max-width: 320px; height: auto; display: block; margin: 0 auto;
}
.em-tacho-aside .em-gm-readout {
font-family: 'Inter', monospace; font-variant-numeric: tabular-nums;
font-weight: 800; font-size: 2.2rem;
color: var(--ggs-fjord-dark);
margin-top: -10px; line-height: 1.1;
}
.em-tacho-aside .em-gm-unit {
font-size: 1.1rem; color: var(--ggs-text-muted); font-weight: 600;
}
.em-tacho-aside .em-gm-pill {
margin-top: 8px;
padding: 5px 14px;
font-size: 0.85rem;
}
.em-gm-explain {
margin-top: 8px; padding: 8px 10px;
background: var(--ggs-bg); border-radius: 6px;
font-size: 0.78rem; line-height: 1.45; color: var(--ggs-text);
}
.em-gm-explain b { color: var(--ggs-fjord-dark); }
/* === InfoCard „Wusstest du?" zwischen Tagen === */
.em-funfact-overlay {
position: fixed; inset: 0;
background: rgba(31,75,55,0.55);
display: flex; align-items: center; justify-content: center;
z-index: 9100; padding: 16px;
backdrop-filter: blur(2px);
}
.em-funfact-overlay[hidden] { display: none; }
.em-funfact-card {
background: linear-gradient(160deg, #fef6dc, #f4f1e8);
border: 1px solid rgba(232,197,71,0.55);
border-radius: 14px;
padding: 24px 26px;
max-width: 480px; width: 100%;
text-align: center;
box-shadow: 0 12px 36px rgba(20,32,26,0.30);
animation: emFunFactPop 0.4s cubic-bezier(.18,1.4,.6,1);
}
@keyframes emFunFactPop {
from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.em-funfact-icon {
width: 70px; height: 70px; border-radius: 50%;
background: #fff; display: inline-flex;
align-items: center; justify-content: center;
font-size: 2.3rem; margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(232,197,71,0.4);
}
.em-funfact-label {
font-size: 0.72rem; letter-spacing: 0.18em;
text-transform: uppercase; font-weight: 700;
color: var(--ggs-moss-dark, #356b3a); margin-bottom: 6px;
}
.em-funfact-title {
font-size: 1.2rem; font-weight: 800;
color: var(--ggs-fjord-dark); margin: 0 0 10px;
}
.em-funfact-text {
font-size: 0.96rem; line-height: 1.55; margin-bottom: 18px;
}
.em-funfact-text b { color: var(--ggs-fjord-dark); }
/* === Set-Abschluss-Overlay === */
.em-set-summary-overlay {
position: fixed; inset: 0;
background: rgba(31,75,55,0.55);
display: flex; align-items: center; justify-content: center;
z-index: 9000; padding: 16px;
backdrop-filter: blur(2px);
}
.em-set-summary-overlay[hidden] { display: none; }
.em-set-summary-card {
background: #fff; border-radius: 16px;
padding: 24px 28px; max-width: 560px; width: 100%;
max-height: 90vh; overflow-y: auto;
box-shadow: 0 12px 40px rgba(20,32,26,0.30);
position: relative;
animation: emSummaryPop 0.4s cubic-bezier(.18,1.4,.6,1);
}
@keyframes emSummaryPop {
from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.em-set-summary-close {
position: absolute; top: 12px; right: 14px;
background: transparent; border: none; cursor: pointer;
font-size: 1.6rem; color: var(--ggs-text-muted);
width: 32px; height: 32px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
.em-set-summary-close:hover { background: var(--ggs-bg); color: var(--ggs-text); }
.em-set-summary-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.em-set-summary-head h2 { margin: 0; font-size: 1.25rem; color: var(--ggs-fjord-dark); }
.em-set-summary-meta { font-size: 0.78rem; color: var(--ggs-text-muted); margin-top: 2px; }
.em-set-summary-stars {
font-size: 2rem; color: #e8c547; letter-spacing: 0.06em;
text-align: center; margin: 12px 0;
text-shadow: 0 2px 4px rgba(232,197,71,0.25);
}
.em-set-summary-stats {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
margin: 12px 0;
}
.em-set-summary-stats > div {
background: #f5f3ea; border-radius: 8px;
padding: 10px 8px; text-align: center;
}
.em-set-summary-stats b {
display: block; font-size: 1.25rem;
color: var(--ggs-fjord-dark); font-weight: 800;
}
.em-set-summary-stats span {
display: block; font-size: 0.66rem;
color: var(--ggs-text-muted); text-transform: uppercase;
letter-spacing: 0.04em; margin-top: 2px;
}
.em-set-summary-days {
list-style: none; padding: 0; margin: 6px 0 0;
}
.em-set-summary-days li {
padding: 8px 0; border-top: 1px solid rgba(0,0,0,0.06);
font-size: 0.88rem;
}
.em-set-summary-days li:first-child { border-top: none; }
.em-set-summary-ach {
margin-top: 14px; padding: 14px 16px;
background: linear-gradient(135deg, rgba(232,197,71,0.18), rgba(74,124,78,0.10));
border: 1px solid rgba(232,197,71,0.55); border-radius: 12px;
}
.em-set-summary-ach-label {
font-family: inherit; font-weight: 700;
color: var(--ggs-fjord-dark); margin-bottom: 8px;
font-size: 0.95rem; text-align: center;
}
.em-set-summary-ach ul { list-style: none; padding: 0; margin: 0; }
.em-set-summary-ach li {
font-size: 0.85rem; padding: 6px 0;
border-top: 1px dashed rgba(31,75,55,0.18);
line-height: 1.4;
}
.em-set-summary-ach li:first-child { border-top: none; }
.em-ach-emoji { font-size: 1.1rem; margin-right: 4px; }
.em-set-summary-actions {
display: flex; justify-content: flex-end; gap: 10px;
margin-top: 18px; flex-wrap: wrap;
}
/* ---- Blackout-Overlay (Verlierzustand) ---- */
.em-blackout-overlay { background: rgba(60,10,10,0.62); }
.em-blackout-card { border-top: 6px solid #dc2626; }
.em-blackout-head { display: flex; align-items: center; gap: 14px; margin-bottom: 10px; }
.em-blackout-icon { font-size: 2.4rem; animation: emBlackoutFlicker 1.1s ease-in-out infinite; }
@keyframes emBlackoutFlicker { 0%,100% { opacity: 1; } 45% { opacity: .35; } 55% { opacity: .9; } }
.em-blackout-head h2 { margin: 0; font-size: 1.4rem; color: #b91c1c; letter-spacing: .01em; }
.em-blackout-sub { font-size: 0.8rem; color: var(--ggs-text-muted); margin-top: 2px; }
.em-blackout-cause { font-size: 0.95rem; line-height: 1.5; color: var(--ggs-text); margin: 6px 0; }
.em-blackout-tip {
font-size: 0.9rem; line-height: 1.5; margin: 10px 0;
background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px; padding: 10px 12px;
}
.em-blackout-note { font-size: 0.82rem; color: var(--ggs-text-muted); margin-top: 8px; }
.em-blackout-btn { background: #dc2626 !important; }
.em-blackout-btn:hover { background: #b91c1c !important; }
.em-set-summary-actions.em-center { justify-content: center; }
</style>
</head>
<body>
<div class="em-layout">
<header class="ggs-header">
<a href="/schueler" class="ggs-header-logo" aria-label="GeoGraSim — zum Cockpit">
<img src="../../assets/img/bildLogo.png" alt="GeoGraSim" class="ggs-header-logo-icon">
</a>
<div class="ggs-header-separator"></div>
<span class="ggs-header-module">Energiemanager</span>
<div class="ggs-header-spacer"></div>
<div class="ggs-header-actions">
<button class="ggs-btn-ghost" id="btnHelp" type="button"
title="Worum geht es? Onboarding nochmal" aria-label="Onboarding öffnen"
onclick="startOnboarding(true)"></button>
<button class="ggs-btn-ghost muted" id="btnAudioToggle"
type="button" aria-pressed="false"
title="Sounds an/aus" aria-label="Sounds umschalten">🔇</button>
<a class="ggs-btn-ghost" href="/schueler" title="Zurück zum Cockpit"
aria-label="Zurück zum Cockpit">🏠</a>
</div>
</header>
<main class="em-main">
<div class="em-set-flow" id="setFlow"><!-- 6 Set-Knoten, per JS --></div>
<div class="em-set-row">
<div class="em-set-tabs" id="setTabs"><!-- per JS --></div>
</div>
<div class="em-day-head">
<div class="em-day-tabs" id="dayTabs"><!-- per JS --></div>
<span class="em-progress-pill" id="emProgressPill"><!-- Set X/6 · Tag Y/3 --></span>
<span class="em-block-running" id="blockRunning" hidden>Block X / 8</span>
</div>
<h1 id="dayTitle" style="margin: 0 0 4px; font-size:1.2rem; color:var(--ggs-fjord-dark); font-weight:800;">Sonniger Frühlingstag</h1>
<p id="dayDesc" style="margin: 0 0 12px; color:var(--ggs-text-muted); font-size:0.88rem; line-height:1.5;"></p>
<!-- Querschnitt + Tachometer als 2-Spalten-Row -->
<div class="em-cross-row">
<!-- Querschnitt mit zwei Seen + Maschinenhaus -->
<section class="em-cross">
<svg class="em-cross-svg" id="crossSvg" viewBox="0 0 1000 650"
preserveAspectRatio="xMidYMid meet"
aria-label="Querschnitt einer Pumpspeicher-Anlage mit zwei Seen">
<defs>
<linearGradient id="emSkyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#4FC3F7"/>
<stop offset="100%" stop-color="#E1F5FE"/>
</linearGradient>
<linearGradient id="emEarthGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6D4C41"/>
<stop offset="100%" stop-color="#3E2723"/>
</linearGradient>
<linearGradient id="emWaterGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="rgba(3,169,244,0.6)"/>
<stop offset="100%" stop-color="rgba(2,119,189,0.85)"/>
</linearGradient>
<clipPath id="emUpperLakeClip">
<polygon points="660,140 910,140 850,200 680,200"/>
</clipPath>
<clipPath id="emLowerLakeClip">
<polygon points="130,420 383,420 300,480 150,480"/>
</clipPath>
</defs>
<!-- Himmel + Sonne -->
<rect width="1000" height="650" fill="url(#emSkyGrad)"/>
<circle cx="850" cy="80" r="40" fill="#FFF59D" opacity="0.8"/>
<!-- Bergsilhouetten -->
<path d="M 0,400 L 150,220 L 350,380 L 600,150 L 800,280 L 1000,120 L 1000,500 L 0,500 Z"
fill="#CFD8DC" opacity="0.7"/>
<path d="M 0,450 L 250,280 L 500,420 L 800,200 L 1000,300 L 1000,500 L 0,500 Z"
fill="#B0BEC5" opacity="0.5"/>
<!-- Bergmasse mit Höhle (Querschnitt) -->
<path d="M 0,650 L 1000,650 L 1000,50 L 850,200 L 680,200 L 620,250 L 300,480 L 150,480 L 100,500 L 0,500 Z"
fill="url(#emEarthGrad)"/>
<!-- Schnee oben rechts -->
<path d="M 1000,50 L 850,200 L 890,200 L 920,160 L 940,180 L 970,120 L 1000,150 Z"
fill="#FFFFFF" opacity="0.9"/>
<!-- Druckrohre (Schale, immer sichtbar) -->
<polyline class="em-pipe-shell" points="750,200 750,330 400,530 330,530"/>
<polyline class="em-pipe-shell" points="270,530 200,530 200,480"/>
<!-- Staumauern -->
<polygon points="650,120 670,120 680,200 620,250" fill="#90A4AE"
stroke="#607D8B" stroke-width="2"/>
<polygon points="120,400 140,400 150,480 100,500" fill="#90A4AE"
stroke="#607D8B" stroke-width="2"/>
<!-- Wasser im Oberbecken (state-driven) -->
<rect class="em-water" id="emWaterTop"
x="660" y="140" width="250" height="60"
clip-path="url(#emUpperLakeClip)"/>
<line x1="660" y1="140" x2="910" y2="140"
stroke="#81D4FA" stroke-width="2"/>
<!-- Wasser im Unterbecken -->
<rect class="em-water" id="emWaterBot"
x="130" y="420" width="253" height="60"
clip-path="url(#emLowerLakeClip)"/>
<line x1="130" y1="420" x2="383" y2="420"
stroke="#81D4FA" stroke-width="2"/>
<!-- Wasser-Flow im Pipe (state-driven via JS-Klassen) -->
<polyline class="em-pipe-flow" id="emFlowA"
points="750,200 750,330 400,530 330,530"
fill="none"/>
<polyline class="em-pipe-flow" id="emFlowB"
points="270,530 200,530 200,480"
fill="none"/>
<!-- Maschinenhaus -->
<rect x="250" y="470" width="100" height="80" rx="10" fill="#BDBDBD"/>
<rect x="255" y="475" width="90" height="70" rx="6" fill="#F5F5F5"/>
<!-- Tür mit zwei Maschinen-Symbolen -->
<rect x="290" y="500" width="20" height="35" rx="3" fill="#D32F2F"/>
<polygon points="310,517 330,505 330,530" fill="#0288D1"/>
<polygon points="290,517 270,505 270,530" fill="#388E3C"/>
<!-- Beschriftungen -->
<text x="780" y="128" font-size="24" fill="#01579B" font-weight="bold"
class="em-cs-label" data-glossar="pumpspeicher" style="cursor:help">Oberbecken</text>
<text x="780" y="102" font-size="28" fill="#01579B" font-weight="bold"
class="em-cs-label" id="emTopPctLabel" data-glossar="gigawattstunde" style="cursor:help">15 / 50 GWh</text>
<text x="180" y="398" font-size="24" fill="#01579B" font-weight="bold"
class="em-cs-label" data-glossar="pumpspeicher" style="cursor:help">Unterbecken</text>
<text x="180" y="372" font-size="28" fill="#01579B" font-weight="bold"
class="em-cs-label" id="emBotPctLabel" data-glossar="gigawattstunde" style="cursor:help">35 / 50 GWh</text>
<text x="300" y="468" font-size="20" fill="#3E2723" font-weight="bold"
text-anchor="middle" class="em-cs-label" data-glossar="turbine" style="cursor:help">Maschinenhaus</text>
</svg>
</section>
<!-- Tachometer-Aside rechts neben dem Querschnitt -->
<aside class="em-tacho-aside" aria-label="Netzfrequenz-Tachometer">
<div class="em-gm-title"><span class="em-glossar-term" data-glossar="netzfrequenz">Netzfrequenz</span></div>
<div class="em-gm-sub">Soll <b>50,00 Hz</b> · ± 0,2 Hz Toleranz</div>
<svg viewBox="0 0 220 135" class="em-gm-tacho-svg" aria-hidden="true">
<path d="M 20 115 A 90 90 0 0 1 200 115" fill="none" stroke="#e2ddd1" stroke-width="14" stroke-linecap="round"/>
<path d="M 20 115 A 90 90 0 0 1 60 42" fill="none" stroke="#c07a6b" stroke-width="14" stroke-linecap="round"/>
<path d="M 60 42 A 90 90 0 0 1 90 29" fill="none" stroke="#e8c547" stroke-width="14" stroke-linecap="round"/>
<path d="M 90 29 A 90 90 0 0 1 130 29" fill="none" stroke="#5a8a5e" stroke-width="14" stroke-linecap="round"/>
<path d="M 130 29 A 90 90 0 0 1 160 42" fill="none" stroke="#e8c547" stroke-width="14" stroke-linecap="round"/>
<path d="M 160 42 A 90 90 0 0 1 200 115" fill="none" stroke="#c07a6b" stroke-width="14" stroke-linecap="round"/>
<text x="20" y="131" text-anchor="middle" font-size="11" fill="#8a8a8a">49,5</text>
<text x="110" y="18" text-anchor="middle" font-size="12" fill="#4a7c8a" font-weight="700">50,0 Hz</text>
<text x="200" y="131" text-anchor="middle" font-size="11" fill="#8a8a8a">50,5</text>
<line id="gmNeedle" x1="110" y1="115" x2="110" y2="35" stroke="#1f4b37" stroke-width="3" stroke-linecap="round" style="transform-origin: 110px 115px; transition: transform 0.6s cubic-bezier(.5,.05,.3,1)"/>
<circle cx="110" cy="115" r="6" fill="#1f4b37"/>
</svg>
<div class="em-gm-readout"><span id="gmFreqVal">50,00</span> <span class="em-gm-unit">Hz</span></div>
<div class="em-gm-pill" id="gmStatusPill">stabil</div>
</aside>
</div>
<!-- /.em-cross-row -->
<!-- Bedarfs-/Solar-Bargraph -->
<section class="em-graph-zone">
<svg class="em-graph-svg" id="graphSvg" viewBox="0 0 800 140"
preserveAspectRatio="none"
aria-label="Bedarfs- und Solarprofil über 8 Drei-Stunden-Blöcke"></svg>
<div class="em-legend">
<div class="em-legend-row">
<span class="em-legend-swatch demand"></span> Strombedarf
</div>
<div class="em-legend-row">
<span class="em-legend-swatch solar"></span> Solar/Wind verfügbar
</div>
</div>
</section>
<!-- Plan-Grid -->
<section class="em-plan" id="planSection">
<h3>Dein Plan für heute</h3>
<p class="em-plan-help">
Pro 3-Stunden-Block ein Schieberegler mit fünf Stufen:
<b>2 Pumpen</b> · <b>1 Pumpe</b> · <b>aus</b> ·
<b>1 Turbine</b> · <b>2 Turbinen</b>.
Pumpen kostet Strom (lädt den oberen See), Turbinen liefert Strom
(entleert den oberen See). Wenn du fertig bist, klick „Tag starten".
</p>
<div class="em-blocks" id="blocksGrid"><!-- per JS --></div>
<div class="em-actions-row">
<button class="em-btn-primary" id="btnRunDay">Tag starten ▶</button>
</div>
</section>
<!-- Netz-Monitor (Chart): läuft mit, sobald der Tag startet — volle Breite unter dem Querschnitt -->
<section class="em-grid-monitor" id="gridMonitor" aria-label="Stromnetz-Frequenzverlauf">
<div class="em-gm-head">
<div class="em-gm-title">📈 Frequenzverlauf <span class="em-gm-sub">über die 8 Blöcke des Tages</span></div>
</div>
<div class="em-gm-body">
<div class="em-gm-chart">
<svg viewBox="0 0 1000 220" class="em-gm-chart-svg" id="gmChartSvg" preserveAspectRatio="none" aria-hidden="true">
<rect width="1000" height="200" fill="#fafaf6"/>
<!-- Toleranzbänder (y-Skala: 49.4..50.6 Hz → 0..200, 50.0 = 100) -->
<rect x="0" y="83" width="1000" height="34" fill="#5a8a5e" opacity="0.12"/>
<rect x="0" y="66" width="1000" height="17" fill="#e8c547" opacity="0.14"/>
<rect x="0" y="117" width="1000" height="17" fill="#e8c547" opacity="0.14"/>
<rect x="0" y="34" width="1000" height="32" fill="#c07a6b" opacity="0.12"/>
<rect x="0" y="134" width="1000" height="32" fill="#c07a6b" opacity="0.12"/>
<!-- Y-Achse-Labels rechts -->
<text x="995" y="40" text-anchor="end" font-size="12" fill="#8a8a8a">50,4 Hz</text>
<text x="995" y="105" text-anchor="end" font-size="12" fill="#4a7c8a" font-weight="700">50,00 Hz</text>
<text x="995" y="170" text-anchor="end" font-size="12" fill="#8a8a8a">49,6 Hz</text>
<!-- 50 Hz Mittellinie -->
<line x1="0" y1="100" x2="1000" y2="100" stroke="#4a7c8a" stroke-dasharray="4 4" stroke-width="1.2" opacity="0.6"/>
<!-- 8 Block-Marker auf der X-Achse -->
<g id="gmBlockMarkers" opacity="0.45"></g>
<!-- Verlaufslinie -->
<polyline id="gmChartLine" fill="none" stroke="#1f4b37" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke" points="" />
<!-- X-Achse Basislinie + Block-Labels -->
<line x1="0" y1="200" x2="1000" y2="200" stroke="#1f4b37" stroke-width="1" opacity="0.5"/>
<g id="gmBlockLabels"></g>
</svg>
</div>
</div>
<div class="em-gm-explain" id="gmExplain">
Sobald der Tag startet, läuft das Netz mit <span class="em-glossar-term" data-glossar="netzfrequenz">50 Hz</span>. Sinkt die Frequenz unter <b>49,8 Hz</b>, drohen <span class="em-glossar-term" data-glossar="lastabwurf">Lastabwürfe</span>; unter <b>49,5 Hz</b> ein <span class="em-glossar-term" data-glossar="blackout">Blackout</span>. Steigt sie über <b>50,2 Hz</b>, müssen Erzeuger gedrosselt werden. Dein Plan entscheidet, wie ruhig das Netz läuft.
</div>
</section>
<!-- Auflösung (initial versteckt) -->
<section class="em-resolution" id="resolutionSection" hidden>
<div class="em-result-row">
<div class="em-result-card" id="resultBalance">
<div class="n" id="resultBalanceN">0/8</div>
<div class="l">ausgeglichen</div>
</div>
<div class="em-result-card" id="resultGap">
<div class="n" id="resultGapN">0</div>
<div class="l">GWh Lücke</div>
</div>
<div class="em-result-card" id="resultSurplus">
<div class="n" id="resultSurplusN">0</div>
<div class="l">GWh Überschuss</div>
</div>
</div>
<div class="em-resolution-summary" id="resolutionSummary"></div>
<div class="em-actions-row">
<button class="em-btn-secondary" id="btnRestart">Tag neu starten ↻</button>
<button class="em-btn-primary" id="btnNextDay" hidden>Weiter →</button>
</div>
</section>
</main>
</div>
<!-- ==================== Onboarding-Modal (Pumpspeicher erklärt) ==================== -->
<div class="em-onboard-overlay" id="emOnboardOverlay" hidden role="dialog" aria-labelledby="emOnbTitle">
<div class="em-onboard-card">
<div class="em-onboard-dots" id="emOnbDots"></div>
<h2 class="em-onboard-title" id="emOnbTitle">Willkommen!</h2>
<div class="em-onboard-visual" id="emOnbVisual"></div>
<div class="em-onboard-text" id="emOnbText"></div>
<div class="em-onboard-actions">
<button class="em-btn-secondary" type="button" id="emOnbSkip" onclick="skipOnboarding()">Überspringen</button>
<button class="em-btn-primary" type="button" id="emOnbNext" onclick="nextOnboarding()">Weiter →</button>
</div>
</div>
</div>
<!-- ==================== „Wusstest du?" zwischen Tagen ==================== -->
<div class="em-funfact-overlay" id="emFunFactOverlay" hidden>
<div class="em-funfact-card">
<div class="em-funfact-icon" id="emFunFactIcon">💡</div>
<div class="em-funfact-label">Wusstest du?</div>
<h3 class="em-funfact-title" id="emFunFactTitle"></h3>
<p class="em-funfact-text" id="emFunFactText"></p>
<button class="em-btn-primary" type="button" onclick="closeFunFact()">Verstanden →</button>
</div>
</div>
<!-- ==================== Blackout-Overlay (Verlierzustand) ==================== -->
<div class="em-set-summary-overlay em-blackout-overlay" id="blackoutOverlay" hidden>
<div class="em-set-summary-card em-blackout-card">
<div id="blackoutBody"></div>
<div class="em-set-summary-actions em-center">
<button class="em-btn-primary em-blackout-btn" type="button" onclick="blackoutReplan()">🔧 Plan überarbeiten & neu starten</button>
</div>
</div>
</div>
<!-- ==================== Set-Abschluss-Overlay ==================== -->
<div class="em-set-summary-overlay" id="setSummaryOverlay" hidden>
<div class="em-set-summary-card">
<button class="em-set-summary-close" type="button" onclick="setSummaryClose()" aria-label="Schließen">×</button>
<div id="setSummaryBody"></div>
<div class="em-set-summary-actions">
<button class="em-btn-secondary" type="button" onclick="setSummaryClose()">Schließen</button>
<button class="em-btn-primary" type="button" onclick="setSummaryNext()">Weiter →</button>
</div>
</div>
</div>
<script>
/* ==================== Energiemanager — Engine (Stufe 1) ====================
2 Seen (Top/Bot) + 2 Pumpturbinen-Maschinen + step-by-step Ablauf.
Plan ist Integer pro Block: -2 / -1 / 0 / +1 / +2
(negativ = Pumpe(n) an, positiv = Turbine(n) an)
============================================================================ */
(function () {
'use strict';
// ---------- Sets + Tage ----------
// 6 thematische Sets à 3 Tagen. Pro Block ist GENAU EINE der fünf Stufen
// ausgleichend (Toleranz ±0.5 GWh).
// Stufen-Strom-Effekte: 1T +5.1 · 2T +10.2 · 1P 5.33 · 2P 10.67
// Tank-Init pro Tag so gewählt, dass die Optimum-Sequenz nirgends auf
// 0 oder 50 GWh anstößt.
var SETS = [
{
id: 'fruehling', title: 'Frühling', icon: '🌱',
days: [
{
id: 'sonnig', shortLabel: 'Tag 1',
title: 'Sonniger Frühlingstag',
desc: 'Gleichmäßiger Bedarf, mittags reichlich Solar. Klassischer Pump-Tag.',
tankTopInit: 20,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 10 }, // 1P
{ range: '09 12', demand: 5, solar: 16 }, // 2P
{ range: '12 15', demand: 5, solar: 16 }, // 2P
{ range: '15 18', demand: 5, solar: 5 }, // Aus
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 10, solar: 0 }, // 2T
],
},
{
id: 'bewoelkt', shortLabel: 'Tag 2',
title: 'Bewölkter Werktag',
desc: 'Dichte Wolken, Solar nur schwach. Speicher voll, viel turbinieren.',
tankTopInit: 40,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 10, solar: 0 }, // 2T
{ range: '06 09', demand: 10, solar: 5 }, // 1T
{ range: '09 12', demand: 5, solar: 10 }, // 1P
{ range: '12 15', demand: 5, solar: 10 }, // 1P
{ range: '15 18', demand: 10, solar: 5 }, // 1T
{ range: '18 21', demand: 10, solar: 0 }, // 2T
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
{
id: 'sturm', shortLabel: 'Tag 3',
title: 'Sturmtag mit Nachtwind',
desc: 'Wind nachts und am Morgen, tagsüber bedeckt. Tagesgang umgedreht.',
tankTopInit: 25,
blocks: [
{ range: '00 03', demand: 5, solar: 10 }, // 1P
{ range: '03 06', demand: 5, solar: 16 }, // 2P
{ range: '06 09', demand: 10, solar: 5 }, // 1T
{ range: '09 12', demand: 5, solar: 5 }, // Aus
{ range: '12 15', demand: 5, solar: 5 }, // Aus
{ range: '15 18', demand: 10, solar: 0 }, // 2T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 5, solar: 10 }, // 1P
],
},
],
},
{
id: 'sommer', title: 'Sommer', icon: '🌞',
days: [
{
id: 'hitzewelle', shortLabel: 'Tag 1',
title: 'Hitzewelle',
desc: 'Klimaanlagen drehen auf — Bedarf nachmittags extrem. Mittags Solar voll.',
tankTopInit: 35,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 10, solar: 15 }, // 1P
{ range: '09 12', demand: 10, solar: 20 }, // 2P
{ range: '12 15', demand: 15, solar: 20 }, // 1P
{ range: '15 18', demand: 20, solar: 10 }, // 2T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 10, solar: 0 }, // 2T
],
},
{
id: 'gewitter', shortLabel: 'Tag 2',
title: 'Sommergewitter',
desc: 'Wolken vorm Mittag — Solar fällt aus. Speicher aus dem ersten Tag muss reichen.',
tankTopInit: 42,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 10, solar: 10 }, // Aus
{ range: '09 12', demand: 10, solar: 5 }, // 1T
{ range: '12 15', demand: 5, solar: 5 }, // Aus
{ range: '15 18', demand: 10, solar: 5 }, // 1T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
{
id: 'saharaluft', shortLabel: 'Tag 3',
title: 'Saharaluft',
desc: 'Klare Luft, viel Solar, Bedarf moderat. Solar-Überschuss füllt den Speicher.',
tankTopInit: 20,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 10 }, // 1P
{ range: '09 12', demand: 5, solar: 16 }, // 2P
{ range: '12 15', demand: 5, solar: 16 }, // 2P
{ range: '15 18', demand: 5, solar: 10 }, // 1P
{ range: '18 21', demand: 10, solar: 5 }, // 1T
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
],
},
{
id: 'herbst', title: 'Herbst', icon: '🍂',
days: [
{
id: 'goldenerherbst', shortLabel: 'Tag 1',
title: 'Goldener Herbst',
desc: 'Klare Tage, gleichmäßiger Wechsel. Solar mittags noch ergiebig.',
tankTopInit: 30,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 5 }, // Aus
{ range: '09 12', demand: 5, solar: 15 }, // 2P
{ range: '12 15', demand: 5, solar: 15 }, // 2P
{ range: '15 18', demand: 10, solar: 5 }, // 1T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 10, solar: 5 }, // 1T
],
},
{
id: 'foehn', shortLabel: 'Tag 2',
title: 'Föhnstunden',
desc: 'Starker Wind durchgehend, Solar moderat. Pumpe und Turbine wechseln sich ständig ab.',
tankTopInit: 15,
blocks: [
{ range: '00 03', demand: 5, solar: 10 }, // 1P
{ range: '03 06', demand: 5, solar: 10 }, // 1P
{ range: '06 09', demand: 10, solar: 5 }, // 1T
{ range: '09 12', demand: 5, solar: 10 }, // 1P
{ range: '12 15', demand: 10, solar: 5 }, // 1T
{ range: '15 18', demand: 5, solar: 10 }, // 1P
{ range: '18 21', demand: 10, solar: 5 }, // 1T
{ range: '21 24', demand: 5, solar: 10 }, // 1P
],
},
{
id: 'wechseltag', shortLabel: 'Tag 3',
title: 'Wetterwechsel',
desc: 'Morgens Nebel, mittags Wolkenlücken, abends Tief. Sehr unbeständig.',
tankTopInit: 42,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 10 }, // 1P
{ range: '09 12', demand: 5, solar: 10 }, // 1P
{ range: '12 15', demand: 10, solar: 0 }, // 2T
{ range: '15 18', demand: 10, solar: 0 }, // 2T
{ range: '18 21', demand: 5, solar: 0 }, // 1T
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
],
},
{
id: 'winter', title: 'Winter', icon: '❄️',
days: [
{
id: 'frostnacht', shortLabel: 'Tag 1',
title: 'Frostnacht',
desc: 'Eisig kalt, Bedarf hoch wegen Heizen. Solar tagsüber knapp, abends Spitze.',
tankTopInit: 42,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 10, solar: 0 }, // 2T
{ range: '06 09', demand: 5, solar: 5 }, // Aus
{ range: '09 12', demand: 5, solar: 10 }, // 1P
{ range: '12 15', demand: 5, solar: 10 }, // 1P
{ range: '15 18', demand: 10, solar: 5 }, // 1T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 10, solar: 0 }, // 2T
],
},
{
id: 'wintersonne', shortLabel: 'Tag 2',
title: 'Sonniger Wintertag',
desc: 'Tagsüber Solar gut, abends sehr kalt. Mit Pumpe-Mittag den Abend vorbereiten.',
tankTopInit: 30,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 5 }, // Aus
{ range: '09 12', demand: 5, solar: 10 }, // 1P
{ range: '12 15', demand: 5, solar: 10 }, // 1P
{ range: '15 18', demand: 10, solar: 5 }, // 1T
{ range: '18 21', demand: 10, solar: 0 }, // 2T
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
{
id: 'schneesturm', shortLabel: 'Tag 3',
title: 'Schneesturm',
desc: 'Heftiger Wind, kaum Solar. Wind hilft tagsüber etwas, abends große Lücke.',
tankTopInit: 30,
blocks: [
{ range: '00 03', demand: 5, solar: 10 }, // 1P
{ range: '03 06', demand: 10, solar: 10 }, // Aus
{ range: '06 09', demand: 10, solar: 5 }, // 1T
{ range: '09 12', demand: 5, solar: 5 }, // Aus
{ range: '12 15', demand: 10, solar: 5 }, // 1T
{ range: '15 18', demand: 10, solar: 5 }, // 1T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 5, solar: 5 }, // Aus
],
},
],
},
{
id: 'industrie', title: 'Industrie', icon: '🏭',
days: [
{
id: 'werktag', shortLabel: 'Tag 1',
title: 'Werktag',
desc: 'Industriegürtel mit hoher Tageslast. Wenig Pump-Spielraum, viel Turbinieren.',
tankTopInit: 40,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 10, solar: 5 }, // 1T
{ range: '09 12', demand: 15, solar: 15 }, // Aus
{ range: '12 15', demand: 15, solar: 20 }, // 1P
{ range: '15 18', demand: 15, solar: 10 }, // 1T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
{
id: 'schichtwechsel', shortLabel: 'Tag 2',
title: 'Schichtwechsel',
desc: 'Drei Bedarfsspitzen statt zwei. Speicher von gestern entscheidet.',
tankTopInit: 50,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 10, solar: 0 }, // 2T
{ range: '06 09', demand: 10, solar: 5 }, // 1T
{ range: '09 12', demand: 5, solar: 5 }, // Aus
{ range: '12 15', demand: 10, solar: 5 }, // 1T
{ range: '15 18', demand: 5, solar: 5 }, // Aus
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 10, solar: 5 }, // 1T
],
},
{
id: 'stillstand', shortLabel: 'Tag 3',
title: 'Werks-Stillstand',
desc: 'Großer Verbraucher legt um — Solar-Überschuss überall. Pumpen, was geht.',
tankTopInit: 20,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 10 }, // 1P
{ range: '09 12', demand: 5, solar: 10 }, // 1P
{ range: '12 15', demand: 5, solar: 16 }, // 2P
{ range: '15 18', demand: 5, solar: 16 }, // 2P
{ range: '18 21', demand: 5, solar: 5 }, // Aus
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
],
},
{
id: 'wochenende', title: 'Wochenende', icon: '🏖️',
days: [
{
id: 'feiertag', shortLabel: 'Tag 1',
title: 'Feiertag',
desc: 'Niedriger Bedarf, normaler Solar-Verlauf. Großer Pump-Tag.',
tankTopInit: 15,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 10 }, // 1P
{ range: '09 12', demand: 5, solar: 16 }, // 2P
{ range: '12 15', demand: 5, solar: 16 }, // 2P
{ range: '15 18', demand: 5, solar: 10 }, // 1P
{ range: '18 21', demand: 5, solar: 5 }, // Aus
{ range: '21 24', demand: 10, solar: 0 }, // 2T
],
},
{
id: 'sonntag', shortLabel: 'Tag 2',
title: 'Ferien-Sonntag',
desc: 'Sehr niedriger Bedarf. Speicher fast unverändert, kaum Bewegung nötig.',
tankTopInit: 20,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 5 }, // Aus
{ range: '09 12', demand: 5, solar: 10 }, // 1P
{ range: '12 15', demand: 5, solar: 10 }, // 1P
{ range: '15 18', demand: 5, solar: 5 }, // Aus
{ range: '18 21', demand: 10, solar: 5 }, // 1T
{ range: '21 24', demand: 5, solar: 0 }, // 1T
],
},
{
id: 'brueckentag', shortLabel: 'Tag 3',
title: 'Brückentag',
desc: 'Eigentlich ruhig, abends überraschende Spitze. Speicher bis dahin retten.',
tankTopInit: 40,
blocks: [
{ range: '00 03', demand: 5, solar: 0 }, // 1T
{ range: '03 06', demand: 5, solar: 0 }, // 1T
{ range: '06 09', demand: 5, solar: 5 }, // Aus
{ range: '09 12', demand: 10, solar: 10 }, // Aus
{ range: '12 15', demand: 10, solar: 15 }, // 1P
{ range: '15 18', demand: 10, solar: 5 }, // 1T
{ range: '18 21', demand: 15, solar: 5 }, // 2T
{ range: '21 24', demand: 10, solar: 0 }, // 2T
],
},
],
},
];
// Aktiver Set + Tag (mutiert beim Wechsel)
var currentSetIdx = 0;
var currentDayIdx = 0;
var DAY = SETS[0].days[0];
// Persistenz zwischen Tagen: wenn der/die Bearbeiter*in via „Weiter →"
// wechselt, wird der Speicherstand von gestern übernommen. Bei direktem
// Tab-Wechsel oder Restart bleibt der Init-Wert pro Tag stehen.
var pendingTankTop = null;
// === Set-Verlauf: Tag-für-Tag-Ergebnisse innerhalb des aktuellen Sets ===
var setRun = { setIdx: 0, startedAt: 0, days: [], blackouts: 0 }; // { setIdx, startedAt:ms, days:[…], blackouts:n }
function resetSetRun(idx) {
setRun = { setIdx: idx, startedAt: Date.now(), days: [], blackouts: 0 };
}
// === Achievements (cross-Set, persistiert in localStorage) ===
var EM_LS_KEY = 'geograsim_energiemanager_v1';
var EM_ACHIEVEMENTS = [
{ id: 'perfect_day', emoji: '🎯', name: 'Punktlandung', desc: 'Einen Tag mit 8 von 8 ausgeglichenen Blöcken abgeschlossen.' },
{ id: 'perfect_set', emoji: '⭐', name: 'Perfektes Set', desc: 'Ein Set mit 24 von 24 Blöcken ausgeglichen.' },
{ id: 'no_blackout', emoji: '🔌', name: 'Stabiles Netz', desc: 'Ein Set komplett ohne Stromlücke.' },
{ id: 'all_seasons', emoji: '🌍', name: 'Vier Jahreszeiten', desc: 'Alle vier Jahreszeiten-Sets gespielt.' },
{ id: 'marathon_em', emoji: '🗺️', name: 'Marathon', desc: 'Alle 6 Sets gespielt.' },
{ id: 'all_5_stars_em', emoji: '🏆', name: 'Bestleistung', desc: 'Alle Sets mit 5 Sternen abgeschlossen.' },
];
function emLsLoad() {
try { return JSON.parse(localStorage.getItem(EM_LS_KEY) || '{}'); } catch (_) { return {}; }
}
function emLsSave(o) {
try { localStorage.setItem(EM_LS_KEY, JSON.stringify(o)); } catch (_) {}
}
function emUnlockAchievement(id) {
var ls = emLsLoad(); ls.achievements = ls.achievements || {};
if (ls.achievements[id]) return false;
ls.achievements[id] = { unlockedAt: Date.now() };
emLsSave(ls);
return true;
}
function emEvaluateAchievements(setSummary) {
// setSummary: {setId, blocksBalanced, blocksTotal, totalGap, perDay:[{balanced},...]}
var newly = [];
// Perfekter Tag
if (setSummary.perDay.some(function(d){ return d.balanced === 8; }) && emUnlockAchievement('perfect_day')) newly.push('perfect_day');
// Perfektes Set
if (setSummary.blocksBalanced === setSummary.blocksTotal && emUnlockAchievement('perfect_set')) newly.push('perfect_set');
// Stabiles Netz (kein Defizit übers Set)
if (setSummary.totalGap < 0.5 && emUnlockAchievement('no_blackout')) newly.push('no_blackout');
// Set-übergreifende Errungenschaften
var ls = emLsLoad();
ls.setsPlayed = ls.setsPlayed || {};
ls.setsPlayed[setSummary.setId] = {
bestStars: Math.max((ls.setsPlayed[setSummary.setId] && ls.setsPlayed[setSummary.setId].bestStars) || 0, setSummary.stars || 0),
plays: ((ls.setsPlayed[setSummary.setId] && ls.setsPlayed[setSummary.setId].plays) || 0) + 1,
lastPlayedAt: Date.now()
};
emLsSave(ls);
// Alle Jahreszeiten
var seasons = ['fruehling','sommer','herbst','winter'];
if (seasons.every(function(s){ return ls.setsPlayed[s]; }) && emUnlockAchievement('all_seasons')) newly.push('all_seasons');
// Marathon: alle Sets
var allIds = SETS.map(function(s){ return s.id; });
if (allIds.every(function(id){ return ls.setsPlayed[id]; }) && emUnlockAchievement('marathon_em')) newly.push('marathon_em');
// Alle 5 Sterne
var all5 = allIds.every(function(id){ return ls.setsPlayed[id] && ls.setsPlayed[id].bestStars === 5; });
if (all5 && emUnlockAchievement('all_5_stars_em')) newly.push('all_5_stars_em');
return newly;
}
// ---------- Sounds ----------
var SOUND_FILES = {
'block-tick': 'block-tick.mp3',
'pump-on': 'pump-on.mp3',
'turbine-on': 'turbine-on.mp3',
'block-ok': 'block-ok.mp3',
'block-warn': 'block-warn.mp3',
'day-good': 'day-good.mp3',
'day-bad': 'day-bad.mp3',
};
var soundCache = {};
var audioEnabled = false; // Default stumm (klassenraum-tauglich)
// Asset-Basispfad — vom Wrapper als window.EM_BASE injiziert
// (z. B. "/sims/energiemanager/" auf Live, "/geograsim/App/sims/energiemanager/" lokal).
// Bei direktem HTML-Aufruf ohne Wrapper: leerer String → relativer Pfad zur game.html.
function emBase() {
return (typeof window.EM_BASE === 'string') ? window.EM_BASE : '';
}
function preloadSounds() {
var base = emBase();
Object.keys(SOUND_FILES).forEach(function (key) {
var a = new Audio(base + 'assets/sounds/' + SOUND_FILES[key]);
a.preload = 'auto';
a.volume = 0.7;
soundCache[key] = a;
});
}
function playSound(key) {
if (!audioEnabled) return;
var a = soundCache[key];
if (!a) return;
try {
a.currentTime = 0;
a.play().catch(function () {});
} catch (e) { /* still */ }
}
var TANK_TOTAL = 50;
var TANK_CAP_TOP = 50;
var TANK_CAP_BOT = 50;
// Eine Maschine pro Block:
var PUMP_WATER_PER_MACHINE = 4; // GWh Wasser nach oben pro Block (1P = 5.33 elec)
var TURB_WATER_PER_MACHINE = 6; // GWh Wasser nach unten pro Block (1T = 5.10 elec)
var PUMP_EFF = 0.75; // Strom-Verbrauch = water / 0.75
var TURB_EFF = 0.85; // Strom-Erzeugung = water * 0.85
var BLOCK_DURATION_MS = 2000; // Ablauf pro Block in der Animation
// ---------- Netzfrequenz-Modell mit echtem Blackout ----------
// Die Netzfrequenz ist kein Deko-Zeiger mehr: Ungleichgewicht schiebt sie weg
// von 50 Hz, die Regelreserve zieht sie zurück. Reicht die Reserve nicht →
// Blackout, der Tag muss neu geplant werden.
// freqError(neu) = freqError(alt) · retain + balance · FREQ_K
// effektive Frequenz = 50 + freqError
var FREQ_K = 0.10; // GWh-Balance → Hz (5 GWh ≈ 0,5 Hz, wie im Tacho)
var FREQ_LOW = 49.5; // darunter: Blackout (physikalische Grenze, siehe Intro)
var FREQ_HIGH = 50.5; // darüber: Blackout durch Überfrequenz
// Schwierigkeitsrampe: früh viel Regelreserve (kleiner retain → schnelle Erholung),
// spät wenig Reserve (großer retain → Fehler summieren sich schneller zum Blackout).
function emRetainForSet(setIdx) {
var table = [0.45, 0.50, 0.55, 0.60, 0.65, 0.70];
return table[Math.min(Math.max(setIdx, 0), table.length - 1)];
}
// ---------- Stufen-Bezeichner ----------
var STAGE_LABELS = {
'-2': '⬆⬆ 2 Pumpen',
'-1': '⬆ 1 Pumpe',
'0': '⏸ Aus',
'1': '⬇ 1 Turbine',
'2': '⬇⬇ 2 Turbinen',
};
var STAGE_CLASSES = {
'-2': 'pump-2', '-1': 'pump-1',
'0': 'off',
'1': 'turb-1','2': 'turb-2',
};
// ---------- State ----------
var state = {
plan: [0,0,0,0,0,0,0,0],
running: false,
finished: false,
currentBlock: 0,
tankTop: DAY.tankTopInit,
tankBot: TANK_TOTAL - DAY.tankTopInit,
timer: null,
results: [],
freqError: 0, // Abweichung von 50 Hz (akkumuliert mit Regelreserve)
blackout: false, // dieser Tag-Lauf endete im Blackout
};
function $(id) { return document.getElementById(id); }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function fmt1(n) { return (Math.round(n * 10) / 10).toFixed(1).replace(/\.0$/,''); }
// ---------- Block-Berechnung ----------
function runBlock(planVal, demand, solar, tankTop, tankBot) {
var pumpMachines = planVal < 0 ? Math.abs(planVal) : 0;
var turbMachines = planVal > 0 ? planVal : 0;
var pumpWaterMax = pumpMachines * PUMP_WATER_PER_MACHINE;
var turbWaterMax = turbMachines * TURB_WATER_PER_MACHINE;
// Caps durch Wasser-Verfügbarkeit bzw. Becken-Kapazität
var pumpWater = Math.min(pumpWaterMax, tankBot, TANK_CAP_TOP - tankTop);
var turbWater = Math.min(turbWaterMax, tankTop, TANK_CAP_BOT - tankBot);
if (pumpWater < 0) pumpWater = 0;
if (turbWater < 0) turbWater = 0;
var pumpElec = pumpWater / PUMP_EFF;
var turbElec = turbWater * TURB_EFF;
var consume = demand + pumpElec;
var supply = solar + turbElec;
var balance = supply - consume; // > 0 Überschuss, < 0 Lücke
var newTop = clamp(tankTop + pumpWater - turbWater, 0, TANK_CAP_TOP);
var newBot = clamp(tankBot - pumpWater + turbWater, 0, TANK_CAP_BOT);
return {
planVal: planVal, demand: demand, solar: solar,
pumpMachines: pumpMachines, turbMachines: turbMachines,
pumpWater: pumpWater, turbWater: turbWater,
pumpElec: pumpElec, turbElec: turbElec,
consume: consume, supply: supply,
balance: balance,
gap: balance < 0 ? -balance : 0,
surplus: balance > 0 ? balance : 0,
tankTopBefore: tankTop, tankTopAfter: newTop,
tankBotBefore: tankBot, tankBotAfter: newBot,
};
}
// ---------- Render: Plan-Grid ----------
function renderBlocks() {
var grid = $('blocksGrid');
var html = '';
DAY.blocks.forEach(function (b, i) {
html += '<div class="em-block" data-i="' + i + '">';
html += '<div class="em-block-label">' + fmtTimeRange(b.range) + '</div>';
html += '<div class="em-block-stats">';
html += '<span class="demand" title="Strombedarf">D ' + b.demand + '</span>';
html += '<span class="solar" title="Solar-Erzeugung">S ' + b.solar + '</span>';
html += '</div>';
html += '<input type="range" class="em-stage-slider" data-i="' + i + '" ' +
'min="-2" max="2" step="1" value="0">';
html += '<div class="em-stage-readout off" data-i="' + i + '">' +
STAGE_LABELS['0'] + '</div>';
html += '<div class="em-block-feedback" data-i="' + i + '" hidden></div>';
html += '</div>';
});
grid.innerHTML = html;
}
function refreshStageReadout(i) {
var v = state.plan[i];
var readout = document.querySelector('.em-stage-readout[data-i="' + i + '"]');
if (!readout) return;
readout.textContent = STAGE_LABELS[String(v)];
readout.className = 'em-stage-readout ' + STAGE_CLASSES[String(v)];
}
// ---------- Render: Bedarfs-/Solar-Bargraph ----------
function drawGraph() {
var svg = $('graphSvg');
var W = 800, H = 140;
var padL = 26, padR = 8, padT = 10, padB = 22;
var innerW = W - padL - padR, innerH = H - padT - padB;
var nB = DAY.blocks.length;
var maxV = Math.max(
Math.max.apply(null, DAY.blocks.map(function (b) { return b.demand; })),
Math.max.apply(null, DAY.blocks.map(function (b) { return b.solar; }))
);
var yMax = Math.ceil((maxV + 2) / 5) * 5;
var colW = innerW / nB;
function xOf(i) { return padL + colW * i; }
function yOf(v) { return padT + innerH - (v / yMax) * innerH; }
var html = '';
for (var g = 0; g <= 4; g++) {
var v = (yMax / 4) * g;
var y = yOf(v);
html += '<line x1="' + padL + '" y1="' + y.toFixed(1) +
'" x2="' + (padL + innerW) + '" y2="' + y.toFixed(1) +
'" stroke="#e2ddd1" stroke-width="0.5"/>';
html += '<text x="' + (padL - 4) + '" y="' + y.toFixed(1) +
'" font-size="9" fill="#999" text-anchor="end" ' +
'alignment-baseline="middle">' + Math.round(v) + '</text>';
}
DAY.blocks.forEach(function (b, i) {
var x0 = xOf(i);
var bw = (colW - 6) / 2;
var hD = (b.demand / yMax) * innerH;
var hS = (b.solar / yMax) * innerH;
html += '<rect x="' + (x0 + 3).toFixed(1) +
'" y="' + (padT + innerH - hD).toFixed(1) +
'" width="' + bw.toFixed(1) +
'" height="' + hD.toFixed(1) + '" fill="#c85c4a" rx="2"/>';
html += '<rect x="' + (x0 + 3 + bw).toFixed(1) +
'" y="' + (padT + innerH - hS).toFixed(1) +
'" width="' + bw.toFixed(1) +
'" height="' + hS.toFixed(1) + '" fill="#e8a82e" rx="2"/>';
html += '<text x="' + (x0 + colW / 2).toFixed(1) +
'" y="' + (H - 6) +
'" font-size="10" fill="#666" text-anchor="middle">' +
fmtTimeRange(b.range) + '</text>';
});
svg.innerHTML = html;
}
// ---------- Wasserstand-Animation ----------
// Oberbecken-clipPath: 660,140 → 910,140 → 850,200 → 680,200
// y=140 (voll), y=200 (leer)
// Unterbecken: 130,420 → 383,420 → 300,480 → 150,480
// y=420 (voll), y=480 (leer)
function updateWaterViz() {
var top = $('emWaterTop'), bot = $('emWaterBot');
if (top) {
var bottomY = 200, topYAtFull = 140;
var depth = (bottomY - topYAtFull) * (state.tankTop / TANK_TOTAL);
top.setAttribute('y', bottomY - depth);
top.setAttribute('height', depth);
}
if (bot) {
var bY = 480, tY = 420;
var d = (bY - tY) * (state.tankBot / TANK_TOTAL);
bot.setAttribute('y', bY - d);
bot.setAttribute('height', d);
}
$('emTopPctLabel').textContent = fmt1(state.tankTop) + ' / ' + TANK_TOTAL + ' GWh';
$('emBotPctLabel').textContent = fmt1(state.tankBot) + ' / ' + TANK_TOTAL + ' GWh';
}
// ---------- Pipe-Flow ----------
function updatePipeFlow(planVal) {
var nodes = document.querySelectorAll('.em-pipe-flow');
if (!nodes.length) return;
var dur = '1.5s';
if (Math.abs(planVal) === 1) dur = '1.4s';
if (Math.abs(planVal) === 2) dur = '0.7s';
nodes.forEach(function (el) {
el.classList.remove('active', 'pump', 'turbine');
el.style.setProperty('--em-flow-dur', dur);
if (planVal > 0) el.classList.add('active', 'turbine');
if (planVal < 0) el.classList.add('active', 'pump');
});
}
// ---------- Step-by-step Ablauf ----------
function runDayStepwise() {
if (state.running || state.finished) return;
state.running = true;
state.tankTop = DAY.tankTopInit;
state.tankBot = TANK_TOTAL - DAY.tankTopInit;
state.results = [];
state.currentBlock = 0;
state.freqError = 0;
state.blackout = false;
gmStartSampling();
// Plan-UI sperren
document.querySelectorAll('.em-stage-slider').forEach(function (s) {
s.disabled = true;
});
$('btnRunDay').disabled = true;
$('btnRunDay').textContent = 'Tag läuft …';
$('blockRunning').hidden = false;
// Tank initial sichtbar
updateWaterViz();
setTimeout(stepNext, 400);
}
function stepNext() {
var i = state.currentBlock;
if (i >= DAY.blocks.length) { showBilanz(); return; }
var blockEl = document.querySelector('.em-block[data-i="' + i + '"]');
if (blockEl) blockEl.classList.add('running');
$('blockRunning').textContent =
'Block ' + (i + 1) + ' / ' + DAY.blocks.length +
' (' + fmtTimeRange(DAY.blocks[i].range) + ')';
// Block-Wechsel-Tick + ggf. Pumpe/Turbine-Sound
playSound('block-tick');
if (state.plan[i] < 0) playSound('pump-on');
else if (state.plan[i] > 0) playSound('turbine-on');
var b = DAY.blocks[i];
var r = runBlock(state.plan[i], b.demand, b.solar, state.tankTop, state.tankBot);
state.results.push(r);
// Netzfrequenz akkumulieren: Ungleichgewicht schiebt weg von 50 Hz,
// die Regelreserve (retain, je Set) zieht zurück. Reicht sie nicht → Blackout.
var retain = emRetainForSet(currentSetIdx);
state.freqError = state.freqError * retain + r.balance * FREQ_K;
var effFreq = 50 + state.freqError;
var blackoutNow = (effFreq < FREQ_LOW || effFreq > FREQ_HIGH);
// Pipe-Flow + Wasserstand-Update (CSS-Transition tut den Rest)
updatePipeFlow(state.plan[i]);
state.tankTop = r.tankTopAfter;
state.tankBot = r.tankBotAfter;
updateWaterViz();
// Netz-Monitor: Zeiger zeigt die AKKUMULIERTE Frequenz (nicht nur diesen Block),
// damit man das Wegdriften bis zum Blackout kommen sieht.
gmSetCurrentBlock(i, state.freqError / FREQ_K);
// Block-Feedback einblenden (kurz vor Block-Ende)
setTimeout(function () {
if (blockEl) {
blockEl.classList.remove('running');
blockEl.classList.add('done');
}
var fb = document.querySelector('.em-block-feedback[data-i="' + i + '"]');
if (fb) {
var label, klass;
if (Math.abs(r.balance) < 0.5) {
label = '✓ ausgegl.'; klass = 'ok';
playSound('block-ok');
} else if (r.balance < 0) {
label = '⚠ Lücke ' + fmt1(-r.balance);
klass = (-r.balance > 3 ? 'bad' : 'warn');
playSound('block-warn');
} else {
label = '⚠ Überschuss ' + fmt1(r.balance);
klass = (r.balance > 3 ? 'bad' : 'warn');
playSound('block-warn');
}
fb.textContent = label;
fb.classList.add(klass);
fb.hidden = false;
}
}, BLOCK_DURATION_MS - 300);
state.currentBlock += 1;
renderProgressPill();
// Bei Blackout den Block noch fertig „spielen" (Zeiger fällt in den roten
// Bereich, Feedback erscheint), dann das Blackout-Overlay öffnen.
if (blackoutNow) {
state.blackout = true;
state.timer = setTimeout(function () { triggerBlackout(i, effFreq, r); }, BLOCK_DURATION_MS);
} else {
state.timer = setTimeout(stepNext, BLOCK_DURATION_MS);
}
}
// ---------- Blackout: echter Verlierzustand ----------
function triggerBlackout(blockIdx, effFreq, r) {
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
state.running = false;
state.finished = false; // Tag ist NICHT abgeschlossen — muss neu geplant werden
state.blackout = true;
gmStopSampling();
setNeedle(effFreq); setReadout(Math.max(49.4, Math.min(50.6, effFreq)));
setStatusPill('⛔ BLACKOUT', 'bad');
updatePipeFlow(0);
playSound('day-bad');
if (setRun.setIdx !== currentSetIdx) resetSetRun(currentSetIdx);
setRun.blackouts = (setRun.blackouts || 0) + 1;
var tooLow = effFreq < FREQ_LOW;
// Blackout sofort ans Lehrer-Backend melden — auch wenn das Set danach
// abgebrochen wird, bleibt der Fehlversuch sichtbar (kein Set-Abschluss nötig).
submitBlackoutToBackend(blockIdx, effFreq, tooLow);
var freqTxt = effFreq.toFixed(2).replace('.', ',');
var cause = tooLow
? ('Die Netzfrequenz ist auf <b>' + freqTxt + ' Hz</b> gefallen — es wurde <b>zu wenig</b> Strom erzeugt. '
+ 'Die Regelreserve war erschöpft, dann brach das Netz zusammen.')
: ('Die Netzfrequenz ist auf <b>' + freqTxt + ' Hz</b> gestiegen — es wurde <b>zu viel</b> Strom erzeugt. '
+ 'Das Netz konnte den Überschuss nicht abführen und brach zusammen.');
var tip = tooLow
? 'Tipp: Wo der Bedarf höher ist als Solar, musst du <b>turbinieren</b> (⬇). Große Lücken brauchen <b>2 Turbinen</b>.'
: 'Tipp: Wo Solar höher ist als der Bedarf, musst du <b>pumpen</b> (⬆). Großen Überschuss fängst du mit <b>2 Pumpen</b>.';
var body = document.getElementById('blackoutBody');
if (body) {
body.innerHTML =
'<div class="em-blackout-head"><div class="em-blackout-icon">🔌</div>'
+ '<div><h2>Blackout!</h2><div class="em-blackout-sub">Block ' + (blockIdx + 1)
+ ' von ' + DAY.blocks.length + ' · ' + DAY.title + '</div></div></div>'
+ '<p class="em-blackout-cause">' + cause + '</p>'
+ '<p class="em-blackout-tip">' + tip + '</p>'
+ '<p class="em-blackout-note">Dein Plan bleibt erhalten — überarbeite die kritischen Blöcke und starte den Tag neu.</p>';
}
var ov = document.getElementById('blackoutOverlay');
if (ov) ov.hidden = false;
}
// Blackout schließen → gleicher Tag, Plan bleibt erhalten, neu startbar.
function blackoutReplan() {
var ov = document.getElementById('blackoutOverlay');
if (ov) ov.hidden = true;
// Lauf-Zustand zurücksetzen, PLAN behalten.
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
state.running = false;
state.finished = false;
state.blackout = false;
state.currentBlock = 0;
state.freqError = 0;
state.results = [];
state.tankTop = DAY.tankTopInit;
state.tankBot = TANK_TOTAL - DAY.tankTopInit;
// Block-Kacheln + Feedback zurücksetzen, Slider wieder freigeben
document.querySelectorAll('.em-block').forEach(function (el) { el.classList.remove('running','done'); });
document.querySelectorAll('.em-block-feedback').forEach(function (fb) { fb.hidden = true; fb.className = 'em-block-feedback'; });
document.querySelectorAll('.em-stage-slider').forEach(function (s) { s.disabled = false; });
updateWaterViz();
updatePipeFlow(0);
gmReset();
$('btnRunDay').disabled = false;
$('btnRunDay').textContent = 'Tag neu starten ▶';
$('blockRunning').hidden = true;
$('resolutionSection').hidden = true;
renderProgressPill();
var grid = document.getElementById('blocksGrid');
if (grid) grid.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
window.blackoutReplan = blackoutReplan;
function showBilanz() {
state.running = false;
state.finished = true;
updatePipeFlow(0);
gmStopSampling(); // Sampling am Tagesende beenden — Verlauf bleibt als Snapshot stehen
$('blockRunning').hidden = true;
$('btnRunDay').textContent = 'Tag fertig';
var balanced = 0, gapTotal = 0, surplusTotal = 0;
state.results.forEach(function (r) {
if (Math.abs(r.balance) < 0.5) balanced += 1;
gapTotal += r.gap;
surplusTotal += r.surplus;
});
$('resultBalanceN').textContent = balanced + '/' + DAY.blocks.length;
$('resultGapN').textContent = fmt1(gapTotal);
$('resultSurplusN').textContent = fmt1(surplusTotal);
var bC = $('resultBalance');
bC.classList.remove('good','warn','bad');
if (balanced >= 7) bC.classList.add('good');
else if (balanced >= 4) bC.classList.add('warn');
else bC.classList.add('bad');
// Tag-Ende-Jingle (verzögert, damit Block-Sounds vorher abklingen)
setTimeout(function () {
playSound(balanced >= 6 ? 'day-good' : 'day-bad');
}, 200);
var gC = $('resultGap');
gC.classList.remove('good','warn','bad');
if (gapTotal < 1) gC.classList.add('good');
else if (gapTotal < 5) gC.classList.add('warn');
else gC.classList.add('bad');
var sC = $('resultSurplus');
sC.classList.remove('good','warn','bad');
if (surplusTotal < 1) sC.classList.add('good');
else if (surplusTotal < 5) sC.classList.add('warn');
else sC.classList.add('bad');
var narrative;
if (balanced === DAY.blocks.length) {
narrative = 'Perfekt geplant. Du hast Solar-Mittag genutzt um den ' +
'oberen See zu füllen, und in der Abendspitze sauber turbiniert.';
} else if (balanced >= 6) {
narrative = 'Gute Plansequenz. Schau dir die rot/orangenen Blöcke ' +
'unten an — dort war der Plan nicht ganz passend.';
} else if (balanced >= 4) {
narrative = 'Geht so. Faustregel: Solar-Säule höher als Bedarf-Säule → ' +
'Pumpen (1 oder 2). Bedarf-Säule höher als Solar → Turbinieren.';
} else {
narrative = 'Da war noch viel Murks dabei. Lies erst die Tageskurve: ' +
'wo ist Solar-Spitze (Mittag), wo ist Bedarfs-Spitze (Abend)? ' +
'Pumpen am Mittag, Turbinieren am Abend ist der Kern.';
}
var initTop = DAY.tankTopInit;
var initBot = TANK_TOTAL - initTop;
// Greifbar machen: 1 Haushalt ~ 3 kWh/Tag → 1 GWh ~ 333.000 Haushalte für 1 Tag, oder ~8 Mio Haushalte für 1 Stunde
var realityNote = '';
if (gapTotal >= 1) {
var households = Math.round(gapTotal * 8000);
realityNote = '<div style="margin-top:6px;color:#555;font-size:0.83rem;line-height:1.45">' +
'<b style="color:var(--ggs-coral)">' + fmt1(gapTotal) + ' <span class="em-glossar-term" data-glossar="gigawattstunde">GWh</span> Lücke</b> bedeutet: ' +
'so viel Strom hätten etwa <b>' + households.toLocaleString('de-AT') + ' Haushalte</b> an einem Tag verbraucht. ' +
'In der Realität würde das Netz hier <b><span class="em-glossar-term" data-glossar="regelreserve">Reserve-Kraftwerke</span></b> hochfahren oder im schlimmsten Fall einen <span class="em-glossar-term" data-glossar="lastabwurf">Lastabwurf</span> auslösen.' +
'</div>';
} else if (surplusTotal >= 1) {
realityNote = '<div style="margin-top:6px;color:#555;font-size:0.83rem;line-height:1.45">' +
'<b style="color:#c89518">' + fmt1(surplusTotal) + ' <span class="em-glossar-term" data-glossar="gigawattstunde">GWh</span> Überschuss</b> heißt: ' +
'<span class="em-glossar-term" data-glossar="photovoltaik">Solar</span>- und Windanlagen müssen abgeregelt werden (verschenkter Strom), oder Nachbarländer importieren günstig. Beides ist wirtschaftlich nicht optimal.' +
'</div>';
} else {
realityNote = '<div style="margin-top:6px;color:var(--ggs-moss-dark);font-size:0.83rem;line-height:1.45">' +
'✓ <b>Netz im Gleichgewicht.</b> Erzeugung deckt Bedarf, keine <span class="em-glossar-term" data-glossar="regelreserve">Reserve</span> nötig. So sollte ein guter Plan-Tag aussehen.' +
'</div>';
}
$('resolutionSummary').innerHTML =
'<h3>Bilanz ' + DAY.title + '</h3><p>' + narrative + '</p>' +
realityNote +
'<p style="margin-top:8px;color:#555;font-size:0.85rem;">' +
'Oberbecken: <b>' + initTop + ' → ' + fmt1(state.tankTop) + ' GWh</b> · ' +
'Unterbecken: <b>' + initBot + ' → ' + fmt1(state.tankBot) + ' GWh</b>' +
'</p>';
// Tag-Ergebnis ins Set-Run-Log aufnehmen (für Set-Abschluss + Backend-Submit)
if (setRun.setIdx !== currentSetIdx) resetSetRun(currentSetIdx);
setRun.days[currentDayIdx] = {
dayId: DAY.id,
dayTitle: DAY.title,
balanced: balanced,
gap: Math.round(gapTotal * 10) / 10,
surplus: Math.round(surplusTotal * 10) / 10,
blocks: DAY.blocks.length
};
// „Weiter →" wenn es noch was gibt
var nextBtn = $('btnNextDay');
var setComplete = (currentDayIdx + 1 >= SETS[currentSetIdx].days.length);
if (hasNext()) {
// Wenn dieser Tag das Set abschließt: Button-Text deutlich machen
nextBtn.textContent = setComplete ? '🏁 Set-Bilanz öffnen →' : nextLabel();
nextBtn.hidden = false;
} else {
nextBtn.textContent = '🏁 Set-Bilanz öffnen →';
nextBtn.hidden = false;
}
$('resolutionSection').hidden = false;
$('resolutionSection').scrollIntoView({ behavior:'smooth', block:'start' });
}
// === Set-Abschluss-Auswertung + Backend-Submit ===
function showSetSummary() {
// Set-Daten sammeln
var setMeta = SETS[setRun.setIdx];
var perDay = setRun.days.filter(Boolean);
var blocksTotal = perDay.reduce(function(s, d){ return s + d.blocks; }, 0);
var blocksBalanced = perDay.reduce(function(s, d){ return s + d.balanced; }, 0);
var totalGap = perDay.reduce(function(s, d){ return s + d.gap; }, 0);
var totalSurplus = perDay.reduce(function(s, d){ return s + d.surplus; }, 0);
// Sterne aus Quote ausgeglichener Blöcke
var quote = blocksTotal > 0 ? blocksBalanced / blocksTotal : 0;
var stars = quote >= 1.00 ? 5
: quote >= 0.87 ? 4
: quote >= 0.66 ? 3
: quote >= 0.50 ? 2 : 1;
// Blackout-Deckelung: wer das Netz zusammenbrechen ließ, kann kein Top-Ergebnis
// holen. 1 Blackout → max 3★, mehrere → max 2★.
var blackouts = setRun.blackouts || 0;
if (blackouts >= 2) stars = Math.min(stars, 2);
else if (blackouts === 1) stars = Math.min(stars, 3);
var summary = {
setId: setMeta.id,
setTitle: setMeta.title,
setIcon: setMeta.icon,
daysTotal: setMeta.days.length,
daysPlayed: perDay.length,
blocksTotal: blocksTotal,
blocksBalanced: blocksBalanced,
totalGap: Math.round(totalGap * 10) / 10,
totalSurplus: Math.round(totalSurplus * 10) / 10,
blackouts: blackouts,
stars: stars,
perDay: perDay
};
// Achievements auswerten + freischalten
var newly = emEvaluateAchievements(summary);
summary.newAchievements = newly;
// Submit an Klassen-Plattform
submitSetToBackend(summary);
// UI: Set-Summary-Modal/-Sektion zeigen
renderSetSummaryUI(summary);
}
// Ein Blackout ist der echte Verlierzustand → sofort als eigenes Fehl-Event
// melden. score:0 färbt den Tag-Balken rot; kein player_progress-Update
// (Blackout gibt keine Sterne/XP). Ursache/Frequenz/Block für die Lehrer-Sicht.
function submitBlackoutToBackend(blockIdx, effFreq, tooLow) {
if (!window.EM_SESSION_ID) return; // Free-Mode: kein Submit
var apiBase = window.EM_API_BASE || '/geograsim/App/api';
var setMeta = SETS[currentSetIdx];
try {
fetch(apiBase + '/progress', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
action: 'blackout',
simId: 'energiemanager',
durationMs: Math.max(0, Date.now() - setRun.startedAt),
data: {
event: 'blackout',
score: 0,
setId: setMeta.id,
setTitle: setMeta.title,
setIcon: setMeta.icon,
day: currentDayIdx + 1,
dayTotal: setMeta.days.length,
dayTitle: DAY.title,
block: blockIdx + 1,
blockTotal: DAY.blocks.length,
freq: Math.round(effFreq * 100) / 100,
cause: tooLow ? 'unterfrequenz' : 'ueberfrequenz',
blackoutSeq: setRun.blackouts || 0
}
})
}).catch(function(e){ console.log('[energiemanager] blackout-submit fehlgeschlagen', e && e.message); });
} catch (_) {}
}
function submitSetToBackend(summary) {
if (!window.EM_SESSION_ID) return; // Free-Mode: kein Submit
var apiBase = window.EM_API_BASE || '/geograsim/App/api';
try {
fetch(apiBase + '/progress', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
action: 'complete',
simId: 'energiemanager',
stars: summary.stars,
durationMs: Math.max(0, Date.now() - setRun.startedAt),
data: summary
})
}).catch(function(e){ console.log('[energiemanager] submit fehlgeschlagen', e && e.message); });
} catch (_) {}
}
function renderSetSummaryUI(s) {
var host = $('setSummaryBody');
if (!host) return;
var starsHtml = '★'.repeat(s.stars) + '<span style="opacity:.3">'+'★'.repeat(5-s.stars)+'</span>';
var perfDaysHtml = s.perDay.map(function(d, i){
var icon = d.balanced === d.blocks ? '🏆' : d.balanced >= 6 ? '✓' : d.balanced >= 4 ? '◐' : '⚠️';
return '<li>'+icon+' <strong>'+d.dayTitle+'</strong> · '+d.balanced+'/'+d.blocks+' ausgegl.'+(d.gap > 0.5 ? ' · '+d.gap+' GWh Lücke' : '')+'</li>';
}).join('');
var achHtml = '';
if (s.newAchievements && s.newAchievements.length) {
achHtml = '<div class="em-set-summary-ach"><div class="em-set-summary-ach-label">🎉 Neu freigeschaltet</div><ul>'
+ s.newAchievements.map(function(id){
var a = EM_ACHIEVEMENTS.filter(function(x){return x.id===id;})[0];
return a ? '<li><span class="em-ach-emoji">'+a.emoji+'</span> <strong>'+a.name+'</strong> — '+a.desc+'</li>' : '';
}).join('')
+ '</ul></div>';
}
var blackoutHtml = (s.blackouts > 0)
? '<div style="margin-top:10px;padding:8px 12px;background:#fef2f2;border:1px solid #fecaca;border-radius:10px;'
+ 'font-size:0.85rem;color:#b91c1c;text-align:center">🔌 <b>'
+ s.blackouts + (s.blackouts === 1 ? ' Blackout' : ' Blackouts')
+ '</b> in diesem Set — deshalb kein Top-Ergebnis. Halte das Netz stabil für mehr Sterne.</div>'
: '';
host.innerHTML =
'<div class="em-set-summary-head">'
+ '<div style="font-size:2rem">'+(s.setIcon||'⚡')+'</div>'
+ '<div><h2>Set '+s.setTitle+' fertig!</h2>'
+ '<div class="em-set-summary-meta">'+s.daysPlayed+' Tage · '+s.blocksBalanced+'/'+s.blocksTotal+' Blöcke ausgeglichen</div></div>'
+ '</div>'
+ '<div class="em-set-summary-stars">'+starsHtml+'</div>'
+ blackoutHtml
+ '<div class="em-set-summary-stats">'
+ '<div><b>'+s.totalGap+'</b> GWh<span>Σ Lücke</span></div>'
+ '<div><b>'+s.totalSurplus+'</b> GWh<span>Σ Überschuss</span></div>'
+ '<div><b>'+s.blocksBalanced+'</b>/'+s.blocksTotal+'<span>ausgeglichen</span></div>'
+ '</div>'
+ '<h3 style="margin-top:14px">Tage im Detail</h3>'
+ '<ul class="em-set-summary-days">'+perfDaysHtml+'</ul>'
+ achHtml;
$('setSummaryOverlay').hidden = false;
}
// === Glossar: Loader + Click-Popup ===
var EM_GLOSSAR_API = (window.EM_API_BASE || '/geograsim/App/php/api') + '/glossar.php';
var EM_GLOSSAR_PAGE = (window.EM_BASE ? window.EM_BASE.replace(/\/sims\/.*$/, '') : '/geograsim/App') + '/glossar';
var EM_GLOSSAR_CACHE = {};
// Lokales Fallback (falls API offline) — nur die wichtigsten EM-spezifischen Begriffe
var EM_GLOSSAR_FALLBACK = {
netzfrequenz: { title:'Netzfrequenz — der Pulsschlag des Stromnetzes (50 Hz)', text:'In unseren Steckdosen fließt kein Gleichstrom wie aus einer Batterie, sondern WECHSELSTROM. Das heißt: Die elektrische Spannung wechselt fünfzig Mal pro Sekunde ihre Richtung — von Plus zu Minus und zurück. Diese Geschwindigkeit nennt man die Netzfrequenz, gemessen in Hertz (Hz).\n\nWarum ist das wichtig? Alle Kraftwerke und alle Verbraucher in Europa hängen am selben Netz und müssen GANZ GENAU im Takt von 50 Hz arbeiten. Stell dir das wie einen riesigen Chor vor: Wenn auch nur ein Sänger zu schnell oder zu langsam singt, klingt es schief.\n\nGenauer: Die Frequenz zeigt, ob Erzeugung und Verbrauch im Gleichgewicht sind:\n• Mehr Strom erzeugt als verbraucht → Frequenz STEIGT über 50 Hz.\n• Mehr Strom verbraucht als erzeugt → Frequenz SINKT unter 50 Hz.\n\nDie Toleranz ist winzig: ±0,2 Hz sind noch normal. Außerhalb davon (49,5 oder 50,5 Hz) greifen automatische Schutzmaßnahmen, damit kein Blackout passiert. Maschinen, Computer und Industrieanlagen sind auf exakt 50 Hz ausgelegt — Abweichungen zerstören sie.\n\nDie Netzfrequenz ist deshalb wie ein Pulsmesser für das ganze Stromnetz Europas — von Portugal bis Polen, von Norwegen bis Sizilien synchron.', unit:'Hz' },
blackout: { title:'Blackout', text:'Großflächiger und längerer Stromausfall. Heizung, Wasser, Ampeln, Internet fallen aus. Kann Stunden bis Tage dauern.' },
lastabwurf: { title:'Lastabwurf', text:'Notmaßnahme bei Frequenz-Einbruch: gezieltes Abschalten großer Stromabnehmer, um Blackout zu verhindern.' },
pumpspeicher: { title:'Pumpspeicherkraftwerk', text:'Zwei Becken in unterschiedlicher Höhe + Turbine. Wasser hochpumpen speichert Energie, ablaufen lassen erzeugt Strom. Wirkungsgrad 70 85 %.' },
turbine: { title:'Turbine', text:'Maschine mit Schaufelrad — durch strömendes Wasser angetrieben, treibt einen Generator an.' },
wirkungsgrad: { title:'Wirkungsgrad', text:'Wie viel Prozent der eingesetzten Energie als Nutz-Energie ankommen. Pumpspeicher: 70 85 %, PV: 20 23 %.' },
photovoltaik: { title:'Photovoltaik', text:'Technik, die Sonnenlicht direkt in Strom umwandelt. Wirkungsgrad 20 23 %.' },
spitzenlast: { title:'Spitzenlast', text:'Höchster Strombedarf des Tages — meist morgens und abends.' },
grundlast: { title:'Grundlast', text:'Strom, der praktisch nie unter einen bestimmten Wert fällt — Kühlschränke, Server, Lüftung etc.' },
lastausgleich:{ title:'Lastausgleich', text:'Aufgabe des Stromnetzes: Erzeugung und Verbrauch ständig in Balance halten.' },
regelreserve: { title:'Regelreserve', text:'Schnell verfügbare Reserve, die in Sekunden bis Minuten zugeschaltet wird, um Frequenz-Schwankungen auszugleichen.' },
stromspeicher:{ title:'Stromspeicher', text:'Anlagen die Strom für später aufbewahren — Pumpspeicher, Batterien, Wasserstoff, Druckluft.' },
dunkelflaute: { title:'Dunkelflaute', text:'Tage mit wenig Wind UND wenig Sonne — Erneuerbare liefern fast nichts.' },
gigawattstunde:{ title:'Gigawattstunde (GWh)', text:'1 GWh = 1 Milliarde Wattstunden — Energie für rund 250.000 Haushalte 1 Tag lang.' },
lastprofil: { title:'Lastprofil', text:'Verlauf des Stromverbrauchs über Stunden oder Tage — zeigt Tagesspitzen und Nachttiefs.' },
residuallast: { title:'Residuallast', text:'Strombedarf NACH Abzug von Wind und Sonne — muss konventionell oder mit Speichern gedeckt werden.' },
stromnetz: { title:'Stromnetz', text:'Verbundnetz aus Leitungen, das Kraftwerke und Verbraucher in ganz Europa synchron auf 50 Hz hält.' },
};
function emLoadGlossar() {
fetch(EM_GLOSSAR_API + '?module=energiemanager').then(function(r){ return r.ok ? r.json() : []; })
.then(function(arr){
if (Array.isArray(arr)) arr.forEach(function(e){
EM_GLOSSAR_CACHE[e.key_slug] = { title:e.title, text:e.short||e.text||'', unit:e.unit||null };
});
})
.catch(function(){ /* still — Fallback greift */ });
}
function emResolveGlossar(key) {
return EM_GLOSSAR_CACHE[key] || EM_GLOSSAR_FALLBACK[key] || null;
}
var _emPopup = null;
document.addEventListener('click', function(e) {
if (_emPopup && !_emPopup.contains(e.target)) { _emPopup.remove(); _emPopup = null; }
var el = e.target.closest('[data-glossar]');
if (!el) return;
var key = el.dataset.glossar;
var entry = emResolveGlossar(key);
if (!entry) return;
e.stopPropagation();
if (_emPopup) { _emPopup.remove(); _emPopup = null; }
var popup = document.createElement('div');
popup.className = 'em-glossar-popup';
var html = '<div class="em-glossar-title">'+entry.title+'</div><div class="em-glossar-text">'+entry.text+'</div>';
if (entry.unit) html += '<div class="em-glossar-unit">Einheit: '+entry.unit+'</div>';
html += '<a class="em-glossar-link" href="'+EM_GLOSSAR_PAGE+'?term='+encodeURIComponent(key)+'" target="_blank" rel="noopener">Voller Glossar-Eintrag →</a>';
popup.innerHTML = html;
document.body.appendChild(popup);
_emPopup = popup;
var rect = el.getBoundingClientRect();
var pw = popup.offsetWidth;
popup.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - pw - 8)) + 'px';
popup.style.top = (rect.bottom + 8) + 'px';
});
emLoadGlossar();
// === Onboarding (4-stufiges Modal mit interaktiver Pumpspeicher-Grafik) ===
// Wird beim ersten Sim-Start automatisch gezeigt (LS-Flag), später über ❔-Header-Button.
var EM_ONBOARDING_STEPS = [
{
title: 'Willkommen — du bist Energiemanager*in!',
visual: '\
<svg viewBox="0 0 460 200" aria-hidden="true">\
<!-- Hintergrund: Berge -->\
<path d="M 0 200 L 0 110 L 80 50 L 160 120 L 240 60 L 340 130 L 460 70 L 460 200 Z" fill="#5a8a5e" opacity="0.7"/>\
<!-- Oberer See -->\
<ellipse cx="120" cy="68" rx="60" ry="9" fill="#1f4b37"/>\
<ellipse cx="120" cy="65" rx="56" ry="6" fill="#4a7c8a"/>\
<text x="120" y="42" text-anchor="middle" font-size="11" font-weight="700" fill="#1f4b37">Oberbecken</text>\
<!-- Unterer See -->\
<ellipse cx="340" cy="172" rx="80" ry="11" fill="#1f4b37"/>\
<ellipse cx="340" cy="168" rx="75" ry="8" fill="#4a7c8a"/>\
<text x="340" y="194" text-anchor="middle" font-size="11" font-weight="700" fill="#1f4b37">Unterbecken</text>\
<!-- Rohrleitung -->\
<path d="M 140 70 L 240 100 L 240 165 L 320 165" stroke="#9E9E9E" stroke-width="9" fill="none" stroke-linecap="round" stroke-linejoin="round"/>\
<path d="M 140 70 L 240 100 L 240 165 L 320 165" stroke="#01579B" stroke-width="3" fill="none" stroke-dasharray="10 6" stroke-linecap="round"><animate attributeName="stroke-dashoffset" from="0" to="-32" dur="1.4s" repeatCount="indefinite"/></path>\
<!-- Maschinenhaus -->\
<rect x="220" y="135" width="44" height="34" fill="#fff" stroke="#1f4b37" stroke-width="1.5" rx="3"/>\
<text x="242" y="156" text-anchor="middle" font-size="9" fill="#1f4b37" font-weight="700">Turbine</text>\
<!-- Blitz oben rechts: Strom raus -->\
<text class="onb-bolt-pulse" x="430" y="40" text-anchor="middle" font-size="28">⚡</text>\
</svg>',
text: 'Willkommen in der Schaltzentrale! Du bist ab jetzt für ein <b><span class="em-glossar-term" data-glossar="pumpspeicher">Pumpspeicherkraftwerk</span></b> verantwortlich. Das ist im Grunde eine riesige Wasser-Batterie in den Bergen: Zwei Stauseen liegen übereinander und sind durch Rohre und eine <span class="em-glossar-term" data-glossar="turbine">Turbine</span> miteinander verbunden.<br><br>Deine Aufgabe heute ist wichtig: Du sorgst dafür, dass eine ganze Stadt immer genug Strom hat — egal ob es mittags hell ist oder ob alle gleichzeitig nach der Arbeit den Herd anschalten. Keine Sorge, ich zeige dir Schritt für Schritt, wie das geht.'
},
{
title: 'Wasser hoch = Strom rein · Wasser runter = Strom raus',
visual: '\
<svg viewBox="0 0 460 200" aria-hidden="true">\
<!-- Linke Hälfte: PUMPEN -->\
<text x="115" y="18" text-anchor="middle" font-size="12" font-weight="700" fill="#00C853">PUMPEN ← Überschuss</text>\
<ellipse cx="115" cy="50" rx="56" ry="7" fill="#4a7c8a"/>\
<ellipse cx="115" cy="158" rx="68" ry="8" fill="#4a7c8a"/>\
<path d="M 115 60 L 115 150" stroke="#9E9E9E" stroke-width="7" fill="none" stroke-linecap="round"/>\
<path class="onb-arrow-pump" d="M 90 130 L 115 110 L 140 130" stroke="#00C853" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>\
<path class="onb-arrow-pump" d="M 115 125 L 115 80" stroke="#00C853" stroke-width="3" fill="none" stroke-linecap="round"/>\
<text x="115" y="186" text-anchor="middle" font-size="10" fill="#1f4b37">Solar-Mittag</text>\
<!-- Rechte Hälfte: TURBINIEREN -->\
<text x="345" y="18" text-anchor="middle" font-size="12" font-weight="700" fill="#01579B">TURBINE → Bedarf</text>\
<ellipse cx="345" cy="50" rx="56" ry="7" fill="#4a7c8a"/>\
<ellipse cx="345" cy="158" rx="68" ry="8" fill="#4a7c8a"/>\
<path d="M 345 60 L 345 150" stroke="#9E9E9E" stroke-width="7" fill="none" stroke-linecap="round"/>\
<path class="onb-arrow-turbine" d="M 320 80 L 345 100 L 370 80" stroke="#01579B" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>\
<path class="onb-arrow-turbine" d="M 345 85 L 345 130" stroke="#01579B" stroke-width="3" fill="none" stroke-linecap="round"/>\
<text x="345" y="186" text-anchor="middle" font-size="10" fill="#1f4b37">Abendspitze</text>\
<!-- Trennlinie -->\
<line x1="230" y1="30" x2="230" y2="170" stroke="#e2ddd1" stroke-dasharray="3 4"/>\
</svg>',
text: 'Das Prinzip ist genial einfach: Wenn die Sonne mittags voll auf die <b><span class="em-glossar-term" data-glossar="photovoltaik">Solaranlagen</span></b> scheint oder Windräder viel Strom erzeugen, hast du oft mehr Energie, als die Stadt gerade braucht. Diesen Überschuss verwendest du, um Wasser vom unteren See nach oben in den oberen See zu pumpen. So speicherst du Energie — quasi als „aufgehobenes Wasser" mit Höhenunterschied.<br><br>Abends, wenn alle nach Hause kommen, kochen, fernsehen und Licht einschalten (das nennt man <span class="em-glossar-term" data-glossar="spitzenlast">Spitzenlast</span>), öffnest du die Schleusen. Das Wasser stürzt durch die Turbine nach unten und erzeugt dabei wieder Strom. Etwa <b>80 %</b> der Energie kommen zurück — das ist der <span class="em-glossar-term" data-glossar="wirkungsgrad">Wirkungsgrad</span>. Die restlichen 20 % gehen unterwegs als Wärme und Reibung verloren.'
},
{
title: 'Plan pro 3-Stunden-Block',
visual: '\
<svg viewBox="0 0 460 200" aria-hidden="true">\
<!-- Tagesachse -->\
<line x1="20" y1="170" x2="440" y2="170" stroke="#1f4b37" stroke-width="1.5"/>\
<text x="20" y="190" font-size="9" fill="#8a8a8a">00 Uhr</text>\
<text x="225" y="190" text-anchor="middle" font-size="9" fill="#8a8a8a">12 Uhr</text>\
<text x="440" y="190" text-anchor="end" font-size="9" fill="#8a8a8a">24 Uhr</text>\
<!-- Bedarf (rot) -->\
<polyline points="20,140 75,135 125,120 175,90 225,60 275,80 325,55 380,75 440,130" fill="none" stroke="#c07a6b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>\
<text x="395" y="40" font-size="10" fill="#c07a6b" font-weight="700">Bedarf</text>\
<!-- Solar (gelb) -->\
<polyline points="20,170 75,170 125,150 175,80 225,40 275,60 325,140 380,170 440,170" fill="none" stroke="#e8c547" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>\
<text x="200" y="32" font-size="10" fill="#c89518" font-weight="700">Solar</text>\
<!-- 8 Blockmarker -->\
<g stroke="#1f4b37" stroke-width="0.6" opacity="0.4" stroke-dasharray="2 3">\
<line x1="72" y1="20" x2="72" y2="170"/>\
<line x1="124" y1="20" x2="124" y2="170"/>\
<line x1="176" y1="20" x2="176" y2="170"/>\
<line x1="228" y1="20" x2="228" y2="170"/>\
<line x1="280" y1="20" x2="280" y2="170"/>\
<line x1="332" y1="20" x2="332" y2="170"/>\
<line x1="384" y1="20" x2="384" y2="170"/>\
</g>\
<!-- Block-Slider-Beispiel unten -->\
<g transform="translate(124, 145)">\
<circle cx="0" cy="0" r="9" fill="#00C853" stroke="#fff" stroke-width="2"/>\
<text x="0" y="3" text-anchor="middle" font-size="9" font-weight="700" fill="#fff">1P</text>\
</g>\
<g transform="translate(280, 80)">\
<circle cx="0" cy="0" r="9" fill="#01579B" stroke="#fff" stroke-width="2"/>\
<text x="0" y="3" text-anchor="middle" font-size="9" font-weight="700" fill="#fff">2T</text>\
</g>\
</svg>',
text: 'Damit du nicht 24 Entscheidungen pro Stunde treffen musst, ist der Tag in <b>8 Blöcke zu je 3 Stunden</b> eingeteilt. Die rote Kurve zeigt das <span class="em-glossar-term" data-glossar="lastprofil">Lastprofil</span> der Stadt — also wie viel Strom in dieser Zeit gebraucht wird. Die gelbe Kurve zeigt, wie viel Strom die Solaranlagen liefern.<br><br>Für jeden Block hast du einen Schieber. Damit stellst du ein, was dein Kraftwerk gerade machen soll: <b>2P</b> heißt stark pumpen (also Wasser nach oben), <b>1P</b> ist sanftes Pumpen, <b>Aus</b> bedeutet die Anlage steht still, <b>1T</b> ist sanftes Turbinieren und <b>2T</b> heißt stark <span class="em-glossar-term" data-glossar="turbine">turbinieren</span> (Wasser nach unten und Strom erzeugen).<br><br>Faustregel zum Merken: <b>Ist die gelbe Solar-Säule höher als die rote Bedarf-Linie, hast du Überschuss</b> — dann lieber pumpen. <b>Ist die rote Linie höher, gibt es zu wenig Strom</b> — dann turbinieren und nachschicken.'
},
{
title: 'Achte aufs Netz: 50 Hz halten!',
visual: '\
<svg viewBox="0 0 460 200" aria-hidden="true">\
<rect width="460" height="200" fill="#fafaf6"/>\
<!-- Toleranzbänder -->\
<rect x="0" y="80" width="460" height="36" fill="#5a8a5e" opacity="0.15"/>\
<rect x="0" y="60" width="460" height="20" fill="#e8c547" opacity="0.18"/>\
<rect x="0" y="116" width="460" height="20" fill="#e8c547" opacity="0.18"/>\
<rect x="0" y="40" width="460" height="20" fill="#c07a6b" opacity="0.15"/>\
<rect x="0" y="136" width="460" height="20" fill="#c07a6b" opacity="0.15"/>\
<line x1="0" y1="98" x2="460" y2="98" stroke="#4a7c8a" stroke-dasharray="3 4"/>\
<text x="455" y="95" text-anchor="end" font-size="9" fill="#4a7c8a">50,00 Hz</text>\
<!-- Demo-Linie: stabil → Lücke → stabil -->\
<polyline points="10,96 30,98 50,95 70,99 90,94 110,100 130,103 150,140 170,150 190,142 210,108 230,98 250,95 270,99 290,94 310,100 330,96 350,97 370,98 390,95 410,100 430,98 450,97" fill="none" stroke="#1f4b37" stroke-width="1.6"/>\
<text x="10" y="190" font-size="9" fill="#1f4b37">Plan ok</text>\
<text x="180" y="190" font-size="9" fill="#c07a6b" font-weight="700">⚠ Lücke</text>\
<text x="290" y="190" font-size="9" fill="#1f4b37">stabil</text>\
</svg>',
text: 'Unser Stromnetz arbeitet mit <b>Wechselstrom</b>. Das heißt, der Strom wechselt fünfzig Mal pro Sekunde seine Richtung — daher die Einheit <b>Hertz (Hz)</b>. Diese Frequenz muss IMMER bei genau <b>50 Hz</b> liegen, sonst funktionieren viele Geräte nicht mehr richtig.<br><br>Im <b>Netz-Monitor</b> unten siehst du diese <span class="em-glossar-term" data-glossar="netzfrequenz">Netzfrequenz</span> live. Sie ist wie das Pulsmessen des ganzen Stromnetzes: Wenn dein Plan zu wenig Strom liefert, ziehen die Verbraucher mehr aus dem Netz, als reinkommt — und die Frequenz sinkt. Sinkt sie unter <b>49,5 Hz</b>, müssen automatisch Stadtteile abgeschaltet werden (das nennt man <span class="em-glossar-term" data-glossar="lastabwurf">Lastabwurf</span>), sonst droht im schlimmsten Fall ein <span class="em-glossar-term" data-glossar="blackout">Blackout</span>.<br><br>Wenn dein Plan zu viel Strom liefert, steigt die Frequenz über 50 Hz — auch das ist gefährlich für Maschinen.<br><br><b>Dein Ziel</b>: die Frequenz so nah wie möglich an 50 Hz halten. Los geht\'s — viel Erfolg!'
}
];
var emOnbStep = 0;
function startOnboarding(forced) {
var ls = emLsLoad();
if (!forced && ls.onboardingDone) return;
emOnbStep = 0;
renderOnboardingStep();
document.getElementById('emOnboardOverlay').hidden = false;
}
function renderOnboardingStep() {
var s = EM_ONBOARDING_STEPS[emOnbStep];
document.getElementById('emOnbTitle').textContent = s.title;
document.getElementById('emOnbVisual').innerHTML = s.visual || '';
document.getElementById('emOnbText').innerHTML = s.text || '';
var dots = EM_ONBOARDING_STEPS.map(function(_, i) {
var cls = i < emOnbStep ? 'done' : i === emOnbStep ? 'active' : '';
return '<div class="dot ' + cls + '"></div>';
}).join('');
document.getElementById('emOnbDots').innerHTML = dots;
var isLast = (emOnbStep === EM_ONBOARDING_STEPS.length - 1);
document.getElementById('emOnbNext').textContent = isLast ? 'Los geht\'s 🚀' : 'Weiter →';
}
function nextOnboarding() {
if (emOnbStep < EM_ONBOARDING_STEPS.length - 1) {
emOnbStep++;
renderOnboardingStep();
} else {
skipOnboarding();
}
}
function skipOnboarding() {
document.getElementById('emOnboardOverlay').hidden = true;
try { var ls = emLsLoad(); ls.onboardingDone = true; emLsSave(ls); } catch (_) {}
}
window.startOnboarding = startOnboarding;
window.nextOnboarding = nextOnboarding;
window.skipOnboarding = skipOnboarding;
// Plattform-Convention: GGS_TUTORIAL (siehe live-client.js).
window.GGS_TUTORIAL = { simId: 'energiemanager', title: 'Energiemanager', replay: function() { startOnboarding(true); } };
// === Netz-Monitor: 50 Hz Soll, lebt während Tag-Lauf ===
// - Sample-Frequenz: alle 70 ms ein Sample → ~28 Samples pro Block (Block ~2 s)
// - Mapping balance → Soll-Frequenz, dazu Rauschen + Transient bei Block-Wechsel
// - 8 Block-Marker auf der X-Achse, Linie spannt über alle 8 Blöcke
var GM = {
timer: null,
samples: [], // [{f, b, wb}] f=freq, b=blockIdx, wb=withinBlockIdx
samplesPerBlock: 28,
currentBlockIdx: 0,
withinBlockIdx: 0,
blockTotal: 8,
currentBalance: 0,
transientUntil: 0,
transientSign: 1,
};
function gmRenderMarkers() {
var markers = document.getElementById('gmBlockMarkers');
var labels = document.getElementById('gmBlockLabels');
if (!markers || !labels) return;
var n = GM.blockTotal;
var m = '', l = '';
var blocks = (DAY && DAY.blocks) || [];
for (var i = 0; i < n; i++) {
var xStart = (i / n) * 1000;
// innere Trennlinie (nicht für i=0)
if (i > 0) {
m += '<line x1="' + xStart.toFixed(1) + '" y1="10" x2="' + xStart.toFixed(1) + '" y2="200" stroke="#1f4b37" stroke-width="1" stroke-dasharray="3 4"/>';
}
var xMid = ((i + 0.5) / n) * 1000;
// Block-Nummer
l += '<text x="' + xMid.toFixed(1) + '" y="216" text-anchor="middle" font-size="13" fill="#1f4b37" font-weight="700">' + (i + 1) + '</text>';
// Zeit-Range darunter klein
if (blocks[i] && blocks[i].range) {
var rangeShort = blocks[i].range.replace(/[-–—]/, ''); // unify dash
l += '<text x="' + xMid.toFixed(1) + '" y="14" text-anchor="middle" font-size="11" fill="#8a8a8a" font-weight="500">' + rangeShort + '</text>';
}
}
markers.innerHTML = m;
labels.innerHTML = l;
}
function balanceToFreq(balance) {
// Soll-Frequenz aus Block-Balance
return 50.00 + balance * 0.10; // -5..+5 → 49.50..50.50
}
function setNeedle(freq) {
var clamped = Math.max(49.4, Math.min(50.6, freq));
var deg = ((clamped - 50.0) / 0.6) * 90;
var el = document.getElementById('gmNeedle');
if (el) el.style.transform = 'rotate(' + deg.toFixed(2) + 'deg)';
}
function setReadout(freq) {
var el = document.getElementById('gmFreqVal');
if (el) el.textContent = freq.toFixed(2).replace('.', ',');
}
function setStatusPill(text, kind) {
var p = document.getElementById('gmStatusPill');
if (!p) return;
p.className = 'em-gm-pill' + (kind === 'warn' ? ' warn' : kind === 'bad' ? ' bad' : '');
p.textContent = text;
}
function gmReset() {
if (GM.timer) { clearInterval(GM.timer); GM.timer = null; }
GM.samples = [];
GM.currentBlockIdx = 0;
GM.withinBlockIdx = 0;
GM.currentBalance = 0;
GM.transientUntil = 0;
GM.blockTotal = (DAY && DAY.blocks) ? DAY.blocks.length : 8;
gmRenderMarkers();
var line = document.getElementById('gmChartLine');
if (line) line.setAttribute('points', '');
setNeedle(50.00); setReadout(50.00); setStatusPill('stabil', 'good');
}
// Bei Tag-Start: kontinuierliches Sampling starten (chaotische Linie)
function gmStartSampling() {
gmReset();
GM.timer = setInterval(gmSampleTick, 70);
}
function gmStopSampling() {
if (GM.timer) { clearInterval(GM.timer); GM.timer = null; }
}
// Vom stepNext aufgerufen — Block-Wechsel meldet seine neue Balance
function gmSetCurrentBlock(blockIdx, balance) {
GM.currentBlockIdx = blockIdx;
GM.withinBlockIdx = 0; // neuer Block → Sub-Index resetten
GM.currentBalance = balance;
// Transient: abklingender Schwinger bei Block-Wechsel (~500 ms)
GM.transientUntil = Date.now() + 500;
GM.transientSign = (balance < 0 ? -1 : balance > 0 ? 1 : (Math.random() < 0.5 ? -1 : 1));
}
function gmSampleTick() {
var now = Date.now();
var base = balanceToFreq(GM.currentBalance);
// Rauschen (Mikrofluktuationen) — kräftiger fürs „chaotisch"-Gefühl
var noise = (Math.random() - 0.5) * 0.09;
// Atmung — drei überlagerte Schwingungen mit unterschiedlichen Frequenzen
var t = now / 1000;
var breath = Math.sin(t * 4.7) * 0.025
+ Math.sin(t * 2.1 + 0.7) * 0.018
+ Math.sin(t * 11.3 + 1.4) * 0.010;
// Transient bei Block-Wechsel
var transient = 0;
if (now < GM.transientUntil) {
var dt = (GM.transientUntil - now) / 500;
transient = GM.transientSign * Math.sin(dt * Math.PI * 3) * 0.14 * dt;
}
var freq = base + noise + breath + transient;
if (freq < 49.4) freq = 49.4;
if (freq > 50.6) freq = 50.6;
// Sample speichern (mit Block- und Sub-Position für X-Mapping)
GM.samples.push({ f: freq, b: GM.currentBlockIdx, wb: GM.withinBlockIdx });
GM.withinBlockIdx += 1;
if (GM.withinBlockIdx >= GM.samplesPerBlock) GM.withinBlockIdx = GM.samplesPerBlock - 1;
drawChart();
setNeedle(freq);
setReadout(freq);
var absDev = Math.abs(freq - 50.0);
if (absDev > 0.40) setStatusPill('⚠ Blackout-Gefahr', 'bad');
else if (absDev > 0.20) setStatusPill('Toleranz überschritten', 'warn');
else setStatusPill('stabil', 'good');
}
function drawChart() {
var line = document.getElementById('gmChartLine');
if (!line) return;
var blockW = 1000 / GM.blockTotal;
var pts = GM.samples.map(function(s) {
var x = (s.b + (s.wb / GM.samplesPerBlock)) * blockW;
var clamped = Math.max(49.4, Math.min(50.6, s.f));
// y-Mapping: 49.4..50.6 → 200..0 (umgedreht, SVG y nach unten)
var y = 200 - ((clamped - 49.4) / 1.2 * 200);
return x.toFixed(1) + ',' + y.toFixed(1);
}).join(' ');
line.setAttribute('points', pts);
}
// Alte updateGridMonitor → No-op (stepNext ruft direkt gmSetCurrentBlock)
function updateGridMonitor(balance) { /* kompatibilitäts-stub */ }
// === InfoCards „Wusstest du?" — zwischen Tagen ===
// Spielt sobald der nächste Tag eines Sets startet, max 1× pro Set-Lauf.
var EM_FUNFACTS = [
{ emoji: '🔋', title: 'Pumpspeicher = riesige Batterie', text: 'Ein <span class="em-glossar-term" data-glossar="pumpspeicher">Pumpspeicherkraftwerk</span> speichert <b>überschüssigen Strom als Wasser auf einem Berg</b>. Das größte in Österreich (Kops II in Vorarlberg) hat 525 MW Leistung — genug für etwa <b>4 Millionen Haushalte</b> auf Knopfdruck.' },
{ emoji: '🎚️', title: 'Wirkungsgrad rund 80 %', text: 'Beim Hochpumpen geht etwa ein Fünftel der Energie als Wärme verloren. Der <span class="em-glossar-term" data-glossar="wirkungsgrad">Wirkungsgrad</span> ist trotzdem unschlagbar gegenüber chemischen Batterien, wenn es um <b>große Mengen</b> über <b>Stunden</b> geht.' },
{ emoji: '📶', title: 'Die magischen 50 Hz', text: 'Das europäische <span class="em-glossar-term" data-glossar="stromnetz">Stromnetz</span> läuft auf <b><span class="em-glossar-term" data-glossar="netzfrequenz">50 Hz</span></b>. Jede Erzeugung muss exakt dem Verbrauch entsprechen — sonst verschiebt sich die Frequenz. Schon <b>±0,2 Hz</b> sind kritisch.' },
{ emoji: '🌅', title: 'Solar-Mittag vs. Abendspitze', text: '<span class="em-glossar-term" data-glossar="photovoltaik">Solar</span> liefert mittags am meisten — aber gebraucht wird Strom abends (<span class="em-glossar-term" data-glossar="spitzenlast">Spitzenlast</span>), wenn Familien heimkommen. Genau dafür gibt es Pumpspeicher: <b>Mittag rauf, Abend runter</b>.' },
{ emoji: '🇦🇹', title: 'Österreich = Stromspeicher Europas', text: 'Mehr als <b>70 %</b> des europäischen <span class="em-glossar-term" data-glossar="stromspeicher">Stromspeichers</span> stehen in den Alpen — vor allem in Österreich, der Schweiz und Norwegen. Wir sind die <b>Batterie Europas</b>.' },
{ emoji: '⏱️', title: 'In Sekunden bereit', text: 'Eine kalte Gas-Turbine braucht Minuten, ein Kohlekraftwerk Stunden. Ein Pumpspeicher steht in <b>weniger als 90 Sekunden</b> bei voller Leistung. Deshalb sind sie <b>Netz-Stabilisatoren Nr. 1</b> — wichtigste <span class="em-glossar-term" data-glossar="regelreserve">Regelreserve</span> im Netz.' },
];
var emFunFactPending = null;
function pickFunFactForDay() {
// Eine FunFact pro Tag-Wechsel, rotierend. Nicht beim allerersten Tag eines Sets.
if (currentDayIdx === 0) return null;
var idx = (currentSetIdx + currentDayIdx) % EM_FUNFACTS.length;
return EM_FUNFACTS[idx];
}
function showFunFact(ff) {
if (!ff) return;
document.getElementById('emFunFactIcon').textContent = ff.emoji || '💡';
document.getElementById('emFunFactTitle').textContent = ff.title || 'Wusstest du?';
document.getElementById('emFunFactText').innerHTML = ff.text || '';
document.getElementById('emFunFactOverlay').hidden = false;
}
function closeFunFact() {
document.getElementById('emFunFactOverlay').hidden = true;
}
window.closeFunFact = closeFunFact;
// Einheitliche Zeit-Formatierung: '00 03' → '00 03 Uhr'
function fmtTimeRange(r) {
if (!r) return '';
// Akzeptiere Bindestrich, en-dash, em-dash und Slash als Trenner
var parts = String(r).split(/[-–—\/]/);
if (parts.length !== 2) return r;
return parts[0].trim() + ' ' + parts[1].trim() + ' Uhr';
}
// ---------- Prozess-Visualisierung: 6 Set-Knoten + Progress-Pill ----------
function renderSetFlow() {
var host = $('setFlow');
if (!host) return;
var ls = emLsLoad();
var setsPlayed = ls.setsPlayed || {};
var html = '';
SETS.forEach(function (s, i) {
var cls = 'em-set-node';
if (i < currentSetIdx) cls += ' done';
else if (i === currentSetIdx) cls += ' active';
var prev = setsPlayed[s.id];
var bestStars = prev && prev.bestStars ? prev.bestStars : 0;
var starsHtml = bestStars > 0
? '<div class="em-set-node-stars">'+'★'.repeat(bestStars)+'</div>'
: '';
html += '<div class="'+cls+'" title="'+s.title+'">'
+ '<div class="em-set-node-dot">'+s.icon+'</div>'
+ '<div class="em-set-node-label">'+s.title+'</div>'
+ starsHtml
+ '</div>';
});
host.innerHTML = html;
}
function renderProgressPill() {
var pill = $('emProgressPill');
if (!pill) return;
var nDays = SETS[currentSetIdx].days.length;
var blockTxt = '';
if (state.running && state.currentBlock != null) {
blockTxt = ' <span class="sep">·</span> Block ' + (state.currentBlock + 1) + '/' + DAY.blocks.length;
} else if (state.finished) {
blockTxt = ' <span class="sep">·</span> Tag fertig';
}
pill.innerHTML = 'Set ' + (currentSetIdx + 1) + '/' + SETS.length
+ ' <span class="sep">·</span> Tag ' + (currentDayIdx + 1) + '/' + nDays
+ blockTxt;
}
// ---------- Set + Tag wechseln ----------
function renderSetTabs() {
var html = '';
SETS.forEach(function (s, i) {
html += '<button class="em-set-tab' +
(i === currentSetIdx ? ' active' : '') +
'" data-set="' + i + '" type="button">' +
s.icon + ' ' + s.title + '</button>';
});
$('setTabs').innerHTML = html;
}
function renderDayTabs() {
var html = '';
SETS[currentSetIdx].days.forEach(function (d, i) {
html += '<button class="em-day-tab' +
(i === currentDayIdx ? ' active' : '') +
'" data-day="' + i + '" type="button">' +
d.shortLabel + '</button>';
});
$('dayTabs').innerHTML = html;
}
function switchSet(idx, keepTank) {
if (idx < 0 || idx >= SETS.length) return;
if (state.running) return;
if (!keepTank) pendingTankTop = null; // direkter Tab-Klick: Reset
currentSetIdx = idx;
currentDayIdx = 0;
resetSetRun(idx);
renderSetTabs();
renderDayTabs();
applyDayChange();
}
function switchDay(idx, keepTank) {
var nDays = SETS[currentSetIdx].days.length;
if (idx < 0 || idx >= nDays) return;
if (state.running) return;
if (!keepTank) pendingTankTop = null; // direkter Tab-Klick: Reset
currentDayIdx = idx;
renderDayTabs();
applyDayChange();
}
function applyDayChange() {
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
DAY = SETS[currentSetIdx].days[currentDayIdx];
$('dayTitle').textContent = DAY.title;
// Persistenter Speicher: wenn pendingTankTop gesetzt ist (von „Weiter →"),
// übernehmen wir den Stand vom Vortag — sonst Tag-Init.
var startTop;
if (pendingTankTop !== null) {
startTop = pendingTankTop;
pendingTankTop = null; // einmaliger Übertrag
$('dayDesc').innerHTML = DAY.desc +
' <b style="color:var(--ggs-fjord-dark)">Speicher von gestern: ' +
fmt1(startTop) + ' GWh.</b>';
} else {
startTop = DAY.tankTopInit;
$('dayDesc').textContent = DAY.desc;
}
state.plan = [0,0,0,0,0,0,0,0];
state.running = false;
state.finished = false;
state.currentBlock = 0;
state.tankTop = startTop;
state.tankBot = TANK_TOTAL - startTop;
state.results = [];
state.freqError = 0;
state.blackout = false;
var boOv = document.getElementById('blackoutOverlay');
if (boOv) boOv.hidden = true;
drawGraph();
renderBlocks();
state.plan.forEach(function (_, i) { refreshStageReadout(i); });
updatePipeFlow(0);
updateWaterViz();
$('btnRunDay').disabled = false;
$('btnRunDay').textContent = 'Tag starten ▶';
$('blockRunning').hidden = true;
$('resolutionSection').hidden = true;
renderSetFlow();
renderProgressPill();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// ---------- Restart (gleicher Tag) ----------
function restart() {
applyDayChange();
}
// Weiter: nächster Tag im Set (mit persistentem Speicher), oder
// — wenn der letzte Tag des Sets eben fertig wurde — Set-Bilanz öffnen.
function nextDay() {
var nDays = SETS[currentSetIdx].days.length;
if (currentDayIdx + 1 < nDays) {
pendingTankTop = state.tankTop; // Speicher vom Vortag mitnehmen
// Erst FunFact zum neuen Tag (didaktischer Häppchen), dann Tag wechseln
var ff = pickFunFactForDay.call(null); // kurz: wir rechnen den NÄCHSTEN Tag-Index gerade
// Wir benutzen currentDayIdx+1 als nächsten Tag, also FunFact basiert auf currentDayIdx+1 sinnvoll
// pickFunFactForDay liest currentDayIdx — daher manuelle Variante:
var nextIdx = currentDayIdx + 1;
var idx = (currentSetIdx + nextIdx) % EM_FUNFACTS.length;
var nextFf = EM_FUNFACTS[idx];
switchDay(currentDayIdx + 1, true);
if (nextFf) showFunFact(nextFf);
} else {
// Set komplett — zeige Set-Bilanz + submit, Wechsel zum nächsten Set passiert über den Modal-Button
showSetSummary();
}
}
// Vom Set-Summary-Modal: zum nächsten Set bzw. zurück zur Übersicht
function setSummaryNext() {
$('setSummaryOverlay').hidden = true;
if (currentSetIdx + 1 < SETS.length) {
switchSet(currentSetIdx + 1, false);
} else {
// Alle Sets fertig — zurück zum ersten Set (oder zur Auswahl)
switchSet(0, false);
}
}
function setSummaryClose() { $('setSummaryOverlay').hidden = true; }
// Diese beiden Funktionen werden aus onclick-Attributen im HTML aufgerufen
// und müssen daher global sein — sonst ReferenceError im Modal.
window.setSummaryClose = setSummaryClose;
window.setSummaryNext = setSummaryNext;
function hasNext() {
var nDays = SETS[currentSetIdx].days.length;
return (currentDayIdx + 1 < nDays) || (currentSetIdx + 1 < SETS.length);
}
function nextLabel() {
var nDays = SETS[currentSetIdx].days.length;
if (currentDayIdx + 1 < nDays) {
var d = SETS[currentSetIdx].days[currentDayIdx + 1];
return 'Weiter zu ' + d.shortLabel + ' →';
}
if (currentSetIdx + 1 < SETS.length) {
return 'Weiter zu ' + SETS[currentSetIdx + 1].icon + ' ' +
SETS[currentSetIdx + 1].title + ' →';
}
return '';
}
// ---------- Live-View für Lehrkräfte ----------
// Plattform-live-client.js ruft alle ~4 s GGS_LIVE_STATE() auf und sendet
// den Snapshot an /api/live. Lehrer-Cockpit zeigt das in der Klassen-Tabelle.
// Atlas-Bitte: klein halten, stabile Schlüssel, didaktisch sinnvoll.
window.GGS_LIVE_STATE = function () {
if (typeof state === 'undefined' || !state) return null;
var phase = state.blackout ? 'blackout'
: state.finished ? 'done'
: state.running ? 'running'
: 'plan';
// Bilanz aus den bisher abgearbeiteten Block-Ergebnissen
var balanced = 0, gap = 0, surplus = 0;
(state.results || []).forEach(function (r) {
if (Math.abs(r.balance) < 0.5) balanced += 1;
gap += r.gap || 0;
surplus += r.surplus || 0;
});
// Set-Summary-Sicht (alle bisher gespielten Tage des aktuellen Sets)
var daysCompleted = (setRun.days || []).filter(Boolean).length;
var setTotal = SETS[currentSetIdx].days.length;
var setBalancedSoFar = (setRun.days || []).reduce(function(s, d){ return s + (d ? d.balanced : 0); }, 0);
// Health-Ampel (cross-app Pflicht-Konvention)
var health = 'good';
var label;
if (phase === 'blackout') {
health = 'struggling';
label = '🔌 Blackout · Tag ' + (currentDayIdx+1) + ' neu planen';
} else if (phase === 'running') {
health = 'good';
label = 'Tag läuft · Block ' + ((state.currentBlock||0)+1) + '/' + DAY.blocks.length;
} else if (phase === 'done') {
if (balanced === DAY.blocks.length) { health = 'good'; }
else if (balanced >= 4) { health = 'ok'; }
else { health = 'struggling'; }
label = 'Tag fertig · ' + balanced + '/' + DAY.blocks.length;
} else {
health = 'good';
label = 'Plant Tag ' + (currentDayIdx+1) + '/' + setTotal;
}
// Achievement-Stand (für Klassen-Überblick)
var achCount = 0;
try { achCount = Object.keys((emLsLoad().achievements)||{}).length; } catch (_) {}
var lastSet = (currentDayIdx + 1 >= setTotal && phase === 'done');
return {
// Sim-Identität + Fortschritt
setId: SETS[currentSetIdx].id,
setTitle: SETS[currentSetIdx].title,
setIcon: SETS[currentSetIdx].icon,
setIndex: currentSetIdx + 1,
setTotal: SETS.length,
day: currentDayIdx + 1,
dayTotal: setTotal,
dayTitle: DAY.title,
block: (state.currentBlock || 0),
blockTotal: DAY.blocks.length,
phase: phase,
// Energie-Werte
upperLake: Math.round(state.tankTop || 0),
lowerLake: Math.round(state.tankBot || 0),
balanced: balanced,
gap: Math.round(gap * 10) / 10,
surplus: Math.round(surplus * 10) / 10,
// Set-Sicht
setDaysDone: daysCompleted,
setBlocksBalanced: setBalancedSoFar,
blackouts: setRun.blackouts || 0,
// Cross-app Konvention
health: health,
healthLabel: label,
levelWon: lastSet && setBalancedSoFar >= setTotal * 6, // Set fertig + ≥75% Blöcke ausgeglichen
achievementsUnlocked: achCount,
achievementsTotal: EM_ACHIEVEMENTS.length,
};
};
// ---------- Init ----------
function init() {
resetSetRun(currentSetIdx);
startOnboarding(false); // beim ersten Mal: Modal zeigen, sonst still
renderSetFlow();
renderSetTabs();
renderDayTabs();
renderProgressPill();
$('dayTitle').textContent = DAY.title;
$('dayDesc').textContent = DAY.desc;
drawGraph();
renderBlocks();
updateWaterViz();
gmReset();
gmRenderMarkers();
state.plan.forEach(function (_, i) { refreshStageReadout(i); });
preloadSounds();
// Audio-Toggle (Sound-Effekte)
var audioBtn = $('btnAudioToggle');
if (audioBtn) {
audioBtn.addEventListener('click', function () {
audioEnabled = !audioEnabled;
audioBtn.classList.toggle('muted', !audioEnabled);
audioBtn.textContent = audioEnabled ? '🔊' : '🔇';
audioBtn.setAttribute('aria-pressed', audioEnabled ? 'true' : 'false');
audioBtn.title = audioEnabled ? 'Sounds aus' : 'Sounds an';
});
}
// Slider-Events
$('blocksGrid').addEventListener('input', function (e) {
var s = e.target;
if (!s.classList.contains('em-stage-slider')) return;
if (state.running) return;
var i = parseInt(s.getAttribute('data-i'), 10);
var v = parseInt(s.value, 10);
if (isNaN(i) || isNaN(v)) return;
state.plan[i] = v;
refreshStageReadout(i);
});
// Set-Tabs (delegated)
$('setTabs').addEventListener('click', function (e) {
var btn = e.target.closest('.em-set-tab');
if (!btn) return;
var idx = parseInt(btn.getAttribute('data-set'), 10);
if (!isNaN(idx)) switchSet(idx);
});
// Tag-Tabs (delegated)
$('dayTabs').addEventListener('click', function (e) {
var btn = e.target.closest('.em-day-tab');
if (!btn) return;
var idx = parseInt(btn.getAttribute('data-day'), 10);
if (!isNaN(idx)) switchDay(idx);
});
$('btnRunDay').addEventListener('click', runDayStepwise);
$('btnRestart').addEventListener('click', restart);
$('btnNextDay').addEventListener('click', nextDay);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>