/**
* Staustufen — Wasserkraft in der Kaskade
* Level 1 — Engine
*
* Ein Durchgang: 5 simulierte Tage à 12 Ticks (= 60 Ticks gesamt).
* Bei normalem Tempo läuft ein Tick alle 1.5 s → komplette Bilanz in 90 s.
* Im 4×-Tempo entsprechend kürzer.
*
* Mechanik (vereinfacht für L1):
* - 2 Speicher (oben/unten), Kapazität je 1000 Einheiten "Wasser-Energie"
* - 1 Pumpe (Unten → Oben), 4 Stufen
* - 1 Turbine (Oben → Unten), 4 Stufen
* - Pump-Wirkungsgrad 75 %, Turbinen-Wirkungsgrad 85 %
* - Konstanter Zufluss 2 Einheiten/Tick in den oberen Speicher
* - Bedarfskurve fix: Tag 1 niedrig, Tag 2 mittel, Tag 3 Spitze,
* Tag 4 niedrig, Tag 5 Spitze
*
* Bewertungs-Modell (Atlas-Update 2026-04-26): Netzbalance-Korridor.
* Pro Tick wird die Abweichung |consume - supply| / demand gemessen:
* ≤ 15 % = grüner Korridor (Netz im Gleichgewicht)
* ≤ 30 % = gelb (Lücke oder Überschuss spürbar)
* > 30 % = rot (kritische Abweichung)
* End-Bewertung: Anteil grüner Ticks + Verlustquote.
*/
(function () {
'use strict';
// ---------- Konstanten ----------
var TICKS_PER_DAY = 12;
var TOTAL_DAYS = 5;
var TOTAL_TICKS = TICKS_PER_DAY * TOTAL_DAYS;
var TANK_CAP = 1000;
// Maximale Wassermengen pro Tick (Volllast). Steuerwert ∈ [-1..+1]
// skaliert linear: pumpWater = -control × MAX_PUMP_RATE (für control < 0),
// turbWater = control × MAX_TURB_RATE (für control > 0). Das ist die
// stufenlose Variante — der Slider gibt jeden Wert zwischen voller Pumpe
// und voller Turbine frei.
var MAX_PUMP_RATE = 16;
var MAX_TURB_RATE = 16;
var PUMP_EFF = 0.75; // benötigte Strom-Einheit / Wasser-Einheit = 1/0.75 ≈ 1.33
var TURB_EFF = 0.85; // erzeugte Strom-Einheit / Wasser-Einheit = 0.85
var INFLOW_PER_TICK = 2; // konstanter natürlicher Zufluss in Tank Oben
var INIT_TOP = 300;
var INIT_BOT = 700;
// Bedarf pro Tick je Tag (Tagesmittel, Strom-Einheiten/Tick)
// Tag 1 niedrig, Tag 2 mittel, Tag 3 Spitze, Tag 4 niedrig, Tag 5 Spitze
var DEMAND_PER_DAY = [6, 12, 22, 6, 22];
// Tageswellenform — Strombedarf schwankt im Tagesverlauf
// (Stunde, Faktor) — Faktoren sind so kalibriert, dass das Tagesmittel
// exakt 1.0 ergibt.
var DEMAND_ANCHORS = [
{ h: 0, v: 0.72 }, // Nacht-Tal
{ h: 8, v: 1.16 }, // Morgenspitze
{ h: 13, v: 0.94 }, // Mittagsdelle
{ h: 19, v: 1.21 }, // Abendspitze
{ h: 24, v: 0.72 }, // wraparound
];
// Mittelwert per Trapez über 24h, gewichtet nach Segmentlänge:
// ((0.72+1.16)/2 * 8 + (1.16+0.94)/2 * 5
// + (0.94+1.21)/2 * 6 + (1.21+0.72)/2 * 5) / 24 ≈ 1.000
var DEMAND_FACTOR_MEAN = 1.000;
// Freie Einspeisung über den Tag — Solar (Mittag-Spitze) + Wind/Lauf
// (niedrige Grundlast nachts). Jeder Tag identisch.
// Tagesmittel ≈ 10.6 — bei Tag 1/4 (Bedarf-Mittel 6) entsteht Überschuss
// tagsüber, bei Tag 3/5 (Bedarf-Mittel 22) eine deutliche Lücke.
var SUPPLY_ANCHORS = [
{ h: 0, v: 6 }, // Nacht: nur Wind/Lauf
{ h: 6, v: 6 }, // Sonnenaufgang
{ h: 9, v: 12 }, // Solar steigt
{ h: 12, v: 22 }, // Mittags-Spitze (Solar)
{ h: 15, v: 18 }, // Nachmittag
{ h: 18, v: 8 }, // Sonnenuntergang
{ h: 21, v: 6 }, // Wind/Lauf
{ h: 24, v: 6 }, // wraparound
];
function supplyFactorAt(hour) {
if (hour < 0) hour = 0;
if (hour > 24) hour = 24;
for (var i = 0; i < SUPPLY_ANCHORS.length - 1; i++) {
var a = SUPPLY_ANCHORS[i], b = SUPPLY_ANCHORS[i + 1];
if (hour >= a.h && hour <= b.h) {
var t = (hour - a.h) / (b.h - a.h);
var s = t * t * (3 - 2 * t);
return a.v + (b.v - a.v) * s;
}
}
return SUPPLY_ANCHORS[0].v;
}
function supplyAtTick(day, tickInDay) {
var hour = tickInDay * 2;
return supplyFactorAt(hour);
}
// Smoothstep-Interpolation zwischen den Stützpunkten — weiche Kurve.
function demandFactorAt(hour) {
if (hour < 0) hour = 0;
if (hour > 24) hour = 24;
for (var i = 0; i < DEMAND_ANCHORS.length - 1; i++) {
var a = DEMAND_ANCHORS[i], b = DEMAND_ANCHORS[i + 1];
if (hour >= a.h && hour <= b.h) {
var t = (hour - a.h) / (b.h - a.h);
var s = t * t * (3 - 2 * t);
return (a.v + (b.v - a.v) * s) / DEMAND_FACTOR_MEAN;
}
}
return DEMAND_ANCHORS[0].v / DEMAND_FACTOR_MEAN;
}
// Bedarf an einem konkreten Tick: Tagesmittel × Wellenfaktor
function demandAtTick(day, tickInDay) {
var hour = tickInDay * 2; // 12 Ticks à 2 Std
if (day < 1) day = 1;
if (day > TOTAL_DAYS) day = TOTAL_DAYS;
return DEMAND_PER_DAY[day - 1] * demandFactorAt(hour);
}
// Tempo (Tick-Intervalle in ms)
// 1× = 5 s/Tick → 5 min Gesamt-Durchgang (L1 ruhig zum Lesen + Reagieren)
// 4× = 1.25 s/Tick → 75 s Vorlauf
var SPEED_MS = { '1x': 5000, '4x': 1250 };
// Netzbalance-Korridor (Anteil der Bedarfshöhe)
var CORRIDOR_GREEN = 0.15; // ≤ 15 % Abweichung = im Gleichgewicht
var CORRIDOR_YELLOW = 0.30; // ≤ 30 % = spürbar, aber nicht kritisch
// ---------- State ----------
var state = {
day: 1, // 1..5
tickInDay: 0, // 0..11 (0 = "00:00", 11 = "22:00")
totalTick: 0, // 0..60
paused: false,
finished: false,
speed: '1x',
rafId: null, // requestAnimationFrame-ID
simProgress: 0, // float, in Tick-Einheiten — pixelweiser Cursor
lastFrameMs: 0, // letzter rAF-Zeitstempel
tankTop: INIT_TOP,
tankBot: INIT_BOT,
// Stufenlose Steuerung — zwischen voller Pumpe (-1) und voller Turbine (+1).
// Mittelpunkt 0 = beides aus. Wird vom bidirektionalen Slider gesetzt.
control: 0,
// Erst-Aktivierungs-Flags, damit InfoCards „pumpe" / „turbine"
// einmalig feuern, sobald ein Wert in die jeweilige Hälfte wandert.
firstPumpUsed: false,
firstTurbineUsed: false,
// Pro Tick aktualisierte Anzeige-Werte
lastTurbineWater: 0,
lastPumpWater: 0,
lastTurbineElec: 0,
lastPumpElec: 0,
// Aktuelle Netzbalance-Klasse: 'green' | 'yellow-deficit' | 'yellow-surplus'
// | 'red-deficit' | 'red-surplus'
lastBalance: 'green',
lastGapRel: 0, // (consume - supply) / demand (signed)
// Pro-Tick-Historie für den Bedarfsgraph (Versorgungslinie)
history: { supply: [] },
totals: {
demand: 0, // gesamt Strom-Einheiten Bedarf (über 5 Tage)
freeSupplyTotal: 0, // gesamt freie Einspeisung
turbineSupplied: 0, // gesamt vom Turbinenstrom geliefert
pumpDrawn: 0, // gesamt vom Pumpen-Strom-Verbrauch
unmet: 0, // gesamt nicht-gedeckte Bedarfslücke
surplus: 0, // gesamt überschüssige Einspeisung
losses: 0, // gesamt Verluste durch Pumpen + Turbinieren
waterMoved: 0, // gesamt bewegtes Wasser
// Netzbalance-Korridor — Tick-Counter
ticksGreen: 0,
ticksYellow: 0,
ticksRed: 0,
},
hintsShown: { welcome: false, imbalance: false },
infocardsShown: {
anlage: false, pumpe: false, turbine: false,
korridor: false, spitze: false,
},
audioEnabled: false, // Default-Mute (Klassenraum-tauglich)
assessment: {
startMs: null,
pingTimer: null,
sentStarted: false,
sentCompleted: false,
},
};
// ---------- Helper ----------
function $(id) { return document.getElementById(id); }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function fmt(n, dec) {
if (typeof dec !== 'number') dec = 0;
if (!isFinite(n)) return '0';
return n.toFixed(dec).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
function pct(v, base) {
if (!base) return 0;
return clamp(Math.round((v / base) * 100), 0, 100);
}
// ---------- Tick-Logik ----------
function tick() {
if (state.paused || state.finished) return;
if (state.totalTick >= TOTAL_TICKS) { finish(); return; }
var demand = demandAtTick(state.day, state.tickInDay);
// 1) Zufluss in oberen Speicher
state.tankTop = clamp(state.tankTop + INFLOW_PER_TICK, 0, TANK_CAP);
// 2) Pumpe Unten → Oben (control < 0) — stufenlos skaliert
var pumpWater = state.control < 0
? Math.abs(state.control) * MAX_PUMP_RATE : 0;
pumpWater = Math.min(pumpWater, state.tankBot, TANK_CAP - state.tankTop);
if (pumpWater < 0) pumpWater = 0;
state.tankBot -= pumpWater;
state.tankTop += pumpWater;
var pumpElec = pumpWater > 0 ? pumpWater / PUMP_EFF : 0;
var pumpLoss = pumpElec - pumpWater;
// 3) Turbine Oben → Unten (control > 0) — stufenlos skaliert
var turbWater = state.control > 0
? state.control * MAX_TURB_RATE : 0;
turbWater = Math.min(turbWater, state.tankTop, TANK_CAP - state.tankBot);
if (turbWater < 0) turbWater = 0;
state.tankTop -= turbWater;
state.tankBot += turbWater;
var turbElec = turbWater * TURB_EFF;
var turbLoss = turbWater - turbElec;
// 4) Bilanz pro Tick — Netzbalance-Korridor
// Verfügbar = freie Einspeisung (Solar/Wind, schwankt) + Turbinen-Strom
// Verbraucht = Bedarf + Pumpen-Strom-Verbrauch
// Korridor wird relativ zum Bedarf gemessen: |gap| / demand
var freeSupply = supplyAtTick(state.day, state.tickInDay);
var supply = freeSupply + turbElec;
var consume = demand + pumpElec;
var gap = consume - supply; // > 0 Lücke, < 0 Überschuss
var unmetT = gap > 0 ? gap : 0;
var surplusT = gap < 0 ? -gap : 0;
var gapRel = demand > 0 ? gap / demand : 0;
var absRel = Math.abs(gapRel);
var balance;
if (absRel <= CORRIDOR_GREEN) {
balance = 'green';
state.totals.ticksGreen += 1;
} else if (absRel <= CORRIDOR_YELLOW) {
balance = gap > 0 ? 'yellow-deficit' : 'yellow-surplus';
state.totals.ticksYellow += 1;
} else {
balance = gap > 0 ? 'red-deficit' : 'red-surplus';
state.totals.ticksRed += 1;
}
state.lastBalance = balance;
state.lastGapRel = gapRel;
// Erstes Mal außerhalb Korridor → InfoCard erklärt das Konzept.
// Erst ab Tick 2, damit nicht direkt nach dem Welcome-Card gefeuert wird.
if (balance !== 'green' && state.totalTick >= 1
&& !state.infocardsShown.korridor) {
showInfoCard('korridor');
}
state.totals.demand += demand;
state.totals.freeSupplyTotal += freeSupply;
state.totals.turbineSupplied += turbElec;
state.totals.pumpDrawn += pumpElec;
state.totals.unmet += unmetT;
state.totals.surplus += surplusT;
state.totals.losses += pumpLoss + turbLoss;
state.totals.waterMoved += pumpWater + turbWater;
// Versorgungs-Historie für den Bedarfsgraph: Netto-Lieferung ans Netz
// (freie Einspeisung + Turbine-Erzeugung − Pump-Verbrauch)
state.history.supply.push(supply - pumpElec);
state.lastTurbineWater = turbWater;
state.lastPumpWater = pumpWater;
state.lastTurbineElec = turbElec;
state.lastPumpElec = pumpElec;
// 5) Tickzähler weiter
state.totalTick += 1;
state.tickInDay += 1;
if (state.tickInDay >= TICKS_PER_DAY) {
state.tickInDay = 0;
state.day += 1;
}
// Spitzen-Tag-InfoCard zu Beginn von Tag 3
if (state.day === 3 && state.tickInDay === 0
&& !state.infocardsShown.spitze) {
showInfoCard('spitze');
}
render();
// Ende erreicht?
if (state.totalTick >= TOTAL_TICKS) {
finish();
return;
}
// Hint #2 — Tag 1, Mitte, falls Bearbeiter*in noch gar nichts angefasst hat
if (state.day === 1 && state.tickInDay === 5 && !state.hintsShown.imbalance
&& state.control === 0) {
showHint('imbalance',
'Tipp: Solar liefert mehr als der Bedarf braucht.',
'Heute (Tag 1) liegt die gelbe Solar-Linie mittags weit ' +
'über der roten Bedarfs-Linie — es gibt Stromüberschuss. ' +
'Wenn du nichts machst, verpufft der einfach. Schieb den Regler ' +
'beim Maschinenhaus nach links (Pumpe), dann fängst du ' +
'den Überschuss ein und hebst Wasser nach oben. Das brauchst du ' +
'für die Abendspitzen in Tag 3 und 5.');
}
}
// ---------- Render ----------
function render() {
// state.day kann nach dem letzten Tick auf 6 stehen (Übergangsmoment vor finish).
var dayIdx = clamp(state.day - 1, 0, TOTAL_DAYS - 1);
var displayDay = dayIdx + 1;
var displayTickInDay = clamp(state.tickInDay, 0, TICKS_PER_DAY - 1);
var demand = demandAtTick(displayDay, displayTickInDay);
// Tag-Pille
var dayCapped = Math.min(state.day, TOTAL_DAYS);
$('dayPill').textContent = 'Tag ' + dayCapped + ' / ' + TOTAL_DAYS;
// Uhrzeit (00:00..22:00 in 2-h-Schritten)
var hour = state.tickInDay * 2;
$('kpiUhrzeit').textContent =
(hour < 10 ? '0' : '') + hour + ':00';
$('kpiTickInfo').textContent =
'Tick ' + (state.totalTick % TICKS_PER_DAY) + ' / ' + TICKS_PER_DAY;
// Netzbalance-KPI: Anteil Ticks im grünen Korridor (bisher)
var ticksDone = state.totals.ticksGreen
+ state.totals.ticksYellow
+ state.totals.ticksRed;
var greenPct = ticksDone > 0
? Math.round((state.totals.ticksGreen / ticksDone) * 100) : 100;
$('kpiNetzbalance').textContent = greenPct + ' %';
var bar = $('kpiNetzbalanceBar');
bar.style.width = greenPct + '%';
bar.classList.remove('warn', 'danger');
if (greenPct < 65) bar.classList.add('danger');
else if (greenPct < 80) bar.classList.add('warn');
// Sub-Zeile mit Lücke + Überschuss für Kontext
var unmetSoFar = state.totals.unmet;
var surplusSoFar = state.totals.surplus;
var subParts = [];
if (unmetSoFar >= 0.5) subParts.push('Lücke ' + fmt(unmetSoFar, 0));
if (surplusSoFar >= 0.5) subParts.push('Überschuss ' + fmt(surplusSoFar, 0));
$('kpiNetzbalanceSub').textContent = subParts.length === 0
? 'Bisher im Gleichgewicht.'
: subParts.join(' · ') + ' GWh';
// Verluste
$('kpiVerluste').textContent = fmt(state.totals.losses, 0) + ' GWh';
// Aktueller Korridor-Status — Live-Indikator
setBalanceIndicator(state.lastBalance, state.lastGapRel);
// Wasserstände auf dem SVG-Querschnitt
var topPct = Math.round((state.tankTop / TANK_CAP) * 100);
var botPct = Math.round((state.tankBot / TANK_CAP) * 100);
renderUpperLake(topPct);
renderLowerLake(botPct);
$('stTopPctLabel').textContent = topPct + ' %';
$('stBotPctLabel').textContent = botPct + ' %';
// Pumpe/Turbine-Symbole + Druckrohr-Animation
renderPipeFlow();
refreshControlUi();
var pumpSym = document.getElementById('stPumpSymbol');
var turbSym = document.getElementById('stTurbSymbol');
if (pumpSym) pumpSym.classList.toggle('active', state.control < -0.02);
if (turbSym) turbSym.classList.toggle('active', state.control > 0.02);
// Graph-Meta (Cursor-Bedarfswert + aktuelle freie Einspeisung)
var freeSupplyNow = supplyAtTick(displayDay, displayTickInDay);
$('graphDemandNow').textContent = fmt(demand, 0);
$('graphSupplyNow').textContent = fmt(freeSupplyNow, 0);
// Graph wird jetzt von der RAF-Schleife pixelweise gerendert —
// hier kein drawGraph()-Aufruf mehr nötig.
}
// Oberbecken — Wasserkörper innerhalb des Mulden-Polygons.
// ClipPath stUpperLakeClip: 660,140 → 910,140 → 850,200 → 680,200
// y=140 (Wasseroberkante bei voller Füllung)
// y=200 (Beckenboden — leer wenn Wasser bis dahin)
function renderUpperLake(pct) {
var el = document.getElementById('stWaterTop');
if (!el) return;
var bottomY = 200, topYAtFull = 140;
var depth = (bottomY - topYAtFull) * (pct / 100);
var y = bottomY - depth;
el.setAttribute('y', y);
el.setAttribute('height', depth);
el.classList.toggle('danger', pct < 15);
}
// Unterbecken — Wasserkörper im Tal.
// ClipPath stLowerLakeClip: 130,420 → 383,420 → 300,480 → 150,480
function renderLowerLake(pct) {
var el = document.getElementById('stWaterBot');
if (!el) return;
var bottomY = 480, topYAtFull = 420;
var depth = (bottomY - topYAtFull) * (pct / 100);
var y = bottomY - depth;
el.setAttribute('y', y);
el.setAttribute('height', depth);
el.classList.toggle('danger', pct < 15);
}
// Druckrohr-Wasseranimation: zeigt Fließrichtung + Geschwindigkeit.
// Stufenlos — Geschwindigkeit ∝ |state.control|. Es können mehrere Pfade
// (Pump-Pfad + Turbinen-Pfad) gleichzeitig animiert werden.
function renderPipeFlow() {
var nodes = document.querySelectorAll('.st-pipe-flow');
if (!nodes.length) return;
var c = state.control;
var mag = Math.abs(c);
// Geschwindigkeit invers zur Magnitude — bei Volllast 0.6 s, sehr schwach
// ~3 s. Min-Cap damit's nicht stehen bleibt bei wirklich kleinem Hub.
var dur = mag > 0.02
? (0.6 + (1 - Math.min(mag, 1)) * 2.4).toFixed(2) + 's'
: '1.5s';
nodes.forEach(function (el) {
el.classList.remove('active', 'pump', 'turbine');
el.style.setProperty('--st-flow-dur', dur);
if (c > 0.02) el.classList.add('active', 'turbine');
if (c < -0.02) el.classList.add('active', 'pump');
});
}
function setBalanceIndicator(balance, gapRel) {
var el = $('kpiKorridor');
var needle = $('livemeterNeedle');
// Magnitude immer positiv — Vorzeichen kommt aus dem Wort-Label
var pctStr = Math.abs(Math.round(gapRel * 100)) + ' %';
// Skala: Erzeugung − Verbrauch (also −gapRel).
// positiv = Überschuss → Nadel rechts (passt zum „+50 % Überschuss"-Label)
// negativ = Lücke → Nadel links (passt zum „−50 % Lücke"-Label)
if (needle) {
var meterValue = -gapRel;
var clamped = Math.max(-0.5, Math.min(0.5, meterValue));
var pos = (clamped + 0.5) * 100; // 0..100 %
needle.style.left = pos.toFixed(1) + '%';
}
if (!el) return;
var label, sub, cls;
switch (balance) {
case 'yellow-deficit':
label = '🟡 Lücke'; sub = 'unter Bedarf · ' + pctStr;
cls = 'st-balance-yellow';
break;
case 'red-deficit':
label = '🔴 Lücke'; sub = 'kritisch unter Bedarf · ' + pctStr;
cls = 'st-balance-red';
break;
case 'yellow-surplus':
label = '🟡 Überschuss'; sub = 'über Bedarf · ' + pctStr;
cls = 'st-balance-yellow';
break;
case 'red-surplus':
label = '🔴 Überschuss'; sub = 'kritisch über Bedarf · ' + pctStr;
cls = 'st-balance-red';
break;
case 'green':
default:
label = '🟢 im Korridor'; sub = '±15 % um den Bedarf';
cls = 'st-balance-green';
break;
}
el.textContent = label;
$('kpiKorridorSub').textContent = sub;
el.classList.remove('st-balance-green', 'st-balance-yellow', 'st-balance-red');
el.classList.add(cls);
}
// ---------- Bedarfs-Graph: Wellenform + Korridor-Band ----------
// Cursor-Position kommt aus state.simProgress (float in Tick-Einheiten),
// nicht aus state.totalTick — dadurch bewegt sich der Cursor pixelweise.
function drawGraph() {
var svg = $('graphSvg');
if (!svg) return;
var W = 1200, H = 240;
var padL = 38, padR = 14, padT = 14, padB = 24;
var innerW = W - padL - padR;
var innerH = H - padT - padB;
// Y-Range: 0 bis Tagesmaximum * Spitzen-Faktor 1.21 + Headroom
var dayMax = Math.max.apply(null, DEMAND_PER_DAY);
var yMax = dayMax * 1.21 * 1.20; // 20 % Headroom über die Spitze
function tx(t) { return padL + (t / TOTAL_TICKS) * innerW; }
function ty(v) { return padT + innerH - (v / yMax) * innerH; }
// Bedarfs- und Einspeisungs-Werte für alle Ticks vorberechnen (deterministic)
var demandVals = [];
var supplyVals = [];
for (var t = 0; t < TOTAL_TICKS; t++) {
var d = Math.floor(t / TICKS_PER_DAY) + 1;
var ti = t % TICKS_PER_DAY;
demandVals.push(demandAtTick(d, ti));
supplyVals.push(supplyAtTick(d, ti));
}
// Bedarfslinie als smooth Path (L-Verbindungen reichen, da 60 Punkte
// bei der Wellenform schon weich aussehen)
var demandPath = '';
var freeSupplyPath = '';
for (var i = 0; i < TOTAL_TICKS; i++) {
demandPath += (i === 0 ? 'M ' : ' L ')
+ tx(i).toFixed(1) + ' ' + ty(demandVals[i]).toFixed(1);
freeSupplyPath += (i === 0 ? 'M ' : ' L ')
+ tx(i).toFixed(1) + ' ' + ty(supplyVals[i]).toFixed(1);
}
// Korridor-Band ±15 % um die Bedarfslinie (geschlossener Pfad)
var corridorPath = '';
// Oberkante (links → rechts)
for (var u = 0; u < TOTAL_TICKS; u++) {
corridorPath += (u === 0 ? 'M ' : ' L ')
+ tx(u).toFixed(1) + ' ' + ty(demandVals[u] * 1.15).toFixed(1);
}
// Unterkante (rechts → links)
for (var lo = TOTAL_TICKS - 1; lo >= 0; lo--) {
corridorPath += ' L '
+ tx(lo).toFixed(1) + ' ' + ty(demandVals[lo] * 0.85).toFixed(1);
}
corridorPath += ' Z';
// Versorgungslinie — nur bis zum aktuellen Tick gezeichnet
var supplyPath = '';
var sup = state.history.supply;
for (var s = 0; s < sup.length; s++) {
supplyPath += (s === 0 ? 'M ' : ' L ')
+ tx(s).toFixed(1) + ' ' + ty(Math.max(0, sup[s])).toFixed(1);
}
// Außerhalb-Korridor-Marker — gelbe/rote Punkte auf der Versorgungslinie
var markerHtml = '';
for (var m = 0; m < sup.length; m++) {
var dem = demandVals[m];
var rel = Math.abs(dem - sup[m]) / dem;
if (rel > 0.15) {
var col = rel > 0.30 ? '#c85c4a' : '#d6a04e';
markerHtml += '
' + def.body + '
'; html += '