Balancing: sonnensystem (Erst-Treffer-Bewertung, Loesung nicht aufdecken, Level-Badge) + energiemanager (echter Blackout an Netzfrequenz, Schwierigkeitsrampe)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -718,6 +718,23 @@
|
||||
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>
|
||||
@@ -1006,6 +1023,16 @@
|
||||
</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">
|
||||
@@ -1365,9 +1392,9 @@
|
||||
var pendingTankTop = null;
|
||||
|
||||
// === Set-Verlauf: Tag-für-Tag-Ergebnisse innerhalb des aktuellen Sets ===
|
||||
var setRun = { setIdx: 0, startedAt: 0, days: [] }; // { setIdx, startedAt:ms, days:[{dayId, dayTitle, balanced, gap, surplus, blocks}] }
|
||||
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: [] };
|
||||
setRun = { setIdx: idx, startedAt: Date.now(), days: [], blackouts: 0 };
|
||||
}
|
||||
|
||||
// === Achievements (cross-Set, persistiert in localStorage) ===
|
||||
@@ -1474,6 +1501,22 @@
|
||||
|
||||
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',
|
||||
@@ -1498,6 +1541,8 @@
|
||||
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); }
|
||||
@@ -1666,6 +1711,8 @@
|
||||
state.tankBot = TANK_TOTAL - DAY.tankTopInit;
|
||||
state.results = [];
|
||||
state.currentBlock = 0;
|
||||
state.freqError = 0;
|
||||
state.blackout = false;
|
||||
gmStartSampling();
|
||||
|
||||
// Plan-UI sperren
|
||||
@@ -1700,13 +1747,21 @@
|
||||
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: Block-Wechsel melden (löst Transient aus + setzt neue Soll-Balance)
|
||||
gmSetCurrentBlock(i, r.balance);
|
||||
// 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 () {
|
||||
@@ -1737,9 +1792,86 @@
|
||||
|
||||
state.currentBlock += 1;
|
||||
renderProgressPill();
|
||||
state.timer = setTimeout(stepNext, BLOCK_DURATION_MS);
|
||||
// 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;
|
||||
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;
|
||||
@@ -1870,6 +2002,11 @@
|
||||
: 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,
|
||||
@@ -1881,6 +2018,7 @@
|
||||
blocksBalanced: blocksBalanced,
|
||||
totalGap: Math.round(totalGap * 10) / 10,
|
||||
totalSurplus: Math.round(totalSurplus * 10) / 10,
|
||||
blackouts: blackouts,
|
||||
stars: stars,
|
||||
perDay: perDay
|
||||
};
|
||||
@@ -1932,6 +2070,12 @@
|
||||
}).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>'
|
||||
@@ -1939,6 +2083,7 @@
|
||||
+ '<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>'
|
||||
@@ -2447,6 +2592,10 @@
|
||||
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();
|
||||
@@ -2527,7 +2676,8 @@
|
||||
window.GGS_LIVE_STATE = function () {
|
||||
if (typeof state === 'undefined' || !state) return null;
|
||||
|
||||
var phase = state.finished ? 'done'
|
||||
var phase = state.blackout ? 'blackout'
|
||||
: state.finished ? 'done'
|
||||
: state.running ? 'running'
|
||||
: 'plan';
|
||||
|
||||
@@ -2547,7 +2697,10 @@
|
||||
// Health-Ampel (cross-app Pflicht-Konvention)
|
||||
var health = 'good';
|
||||
var label;
|
||||
if (phase === 'running') {
|
||||
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') {
|
||||
@@ -2587,6 +2740,7 @@
|
||||
// Set-Sicht
|
||||
setDaysDone: daysCompleted,
|
||||
setBlocksBalanced: setBalancedSoFar,
|
||||
blackouts: setRun.blackouts || 0,
|
||||
// Cross-app Konvention
|
||||
health: health,
|
||||
healthLabel: label,
|
||||
|
||||
Reference in New Issue
Block a user