Files
geograsim/App/admin-lehrer-view.html
T
Adminator 3bc0f418be Bewertungs-Vorschau: EINE Skala-Darstellung + 4-stufige Triage
- Doppeldarstellung (Beispiel-Balken + Legende) zu EINER Tabelle
  zusammengefuehrt: Farbe + Bereich + Label + Beispiel-Balken je Zeile.
  Beispielwert = Bereichsschwelle (90/80/60/50), nicht willkuerlich.
- Triage jetzt 4 Felder an denselben Farbgrenzen:
  <50 Foerdern (Hilfe) / 50-79 Begleiten / 80-89 Laeuft gut /
  >=90 Fordern (herausfordern). Styleguide + Preview angeglichen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 02:51:50 +02:00

826 lines
70 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Lehrer View Parameter-Überblick Schülerleistung</title>
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="stylesheet" href="assets/fonts/inter.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;min-height:100vh;color:#2a2a2a}
.topbar{background:#fff;padding:.6rem 1.2rem;display:flex;align-items:center;gap:.8rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:20;flex-wrap:wrap}
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1;display:flex;align-items:center;gap:.5rem;min-width:220px}
.topbar h1 img{height:26px}
.btn-sm{padding:.3rem .6rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.68rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none;white-space:nowrap}
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
.wrap{max-width:1240px;margin:1rem auto 4rem;padding:0 1rem}
.intro{background:#fff;border-radius:12px;padding:1.1rem 1.3rem;box-shadow:0 1px 4px rgba(0,0,0,.04);margin-bottom:1.2rem}
.intro h2{font-size:1.05rem;color:#1f4b37;font-weight:800;margin-bottom:.4rem}
.intro p{font-size:.82rem;line-height:1.5;color:#4a4a4a;margin-bottom:.5rem}
.intro .wiring{display:flex;gap:.5rem;flex-wrap:wrap;margin:.6rem 0 .2rem}
.wire-chip{background:#eef3f5;border:1px solid #d5e2e7;border-radius:8px;padding:.35rem .7rem;font-size:.72rem;color:#3a5a66;font-weight:600}
.wire-chip b{color:#1f4b37}
.legend{display:flex;gap:1.2rem;flex-wrap:wrap;margin-top:.7rem;padding-top:.7rem;border-top:1px solid rgba(0,0,0,.06);font-size:.76rem;color:#4a4a4a}
.legend span{display:inline-flex;align-items:center;gap:.4rem}
.sw{width:16px;height:16px;border-radius:4px;display:inline-block;border:1px solid rgba(0,0,0,.12)}
.toc{display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:1.4rem}
.toc a{font-size:.72rem;text-decoration:none;background:#fff;border:1px solid rgba(0,0,0,.08);border-radius:20px;padding:.3rem .7rem;color:#3a5a66;font-weight:600}
.toc a:hover{background:#dae8ec}
.sim{background:#fff;border-radius:12px;box-shadow:0 1px 4px rgba(0,0,0,.04);margin-bottom:1.6rem;overflow:hidden;scroll-margin-top:70px}
.sim-head{display:flex;align-items:baseline;gap:.6rem;padding:.9rem 1.1rem .5rem;flex-wrap:wrap}
.sim-head .emoji{font-size:1.5rem;line-height:1}
.sim-head h3{font-size:1.1rem;font-weight:800;color:#1f4b37}
.sim-head .sid{font-size:.68rem;color:#8a8a8a;font-family:'Courier New',monospace}
.sim-head .count{margin-left:auto;font-size:.7rem;color:#5a8a5e;font-weight:700}
.tbl-scroll{overflow-x:auto;padding:0 1.1rem}
table.live{width:100%;border-collapse:collapse;font-size:.74rem}
table.live th{padding:.4rem .5rem;white-space:nowrap;vertical-align:bottom;border-bottom:2px solid rgba(0,0,0,.08)}
table.live th .key{display:block;font-size:.58rem;color:#9a9a9a;font-weight:500;font-family:'Courier New',monospace;margin-top:2px}
table.live th.green{background:#d9eddc}
table.live th.grey{background:#f7f6f3}
table.live td{padding:.4rem .5rem;font-variant-numeric:tabular-nums;vertical-align:top;border-bottom:1px solid rgba(0,0,0,.04)}
table.live td.green{background:#eef7f0}
.tag{display:inline-block;font-size:.55rem;font-weight:700;border-radius:4px;padding:1px 5px;margin-top:3px;letter-spacing:.03em}
.tag.g{background:#5a8a5e;color:#fff}
.tag.l{background:#e3e0d8;color:#7a7568}
.note{margin:.6rem 1.1rem 1rem;background:#faf7f0;border:1px solid #ece3cf;border-left:4px solid #d8b04a;border-radius:8px;padding:.6rem .85rem;font-size:.76rem;color:#6a5a2a}
.note b{color:#8a6a1a}
.note ul{margin:.35rem 0 0 1.1rem;padding:0}
.note li{margin:.15rem 0;line-height:1.4}
.note code{background:#efe8d5;border-radius:3px;padding:0 4px;font-size:.72rem;color:#7a5a1a}
.allgreen{margin:.6rem 1.1rem 1rem;background:#f0f7f1;border:1px solid #cfe6d2;border-left:4px solid #5a8a5e;border-radius:8px;padding:.55rem .85rem;font-size:.76rem;color:#3a6b3e}
.scorebox{margin:.2rem 1.1rem 1rem;background:#eef3f7;border:1px solid #d3e0e8;border-left:4px solid #4a7c8a;border-radius:8px;padding:.7rem .9rem;font-size:.78rem;color:#2f4a58}
.scorebox h4{font-size:.8rem;color:#2f5866;margin-bottom:.45rem;display:flex;align-items:center;gap:.4rem}
.scorebox .srow{display:grid;grid-template-columns:78px 1fr;gap:.5rem;margin:.28rem 0;align-items:start}
.scorebox .srow .lab{font-weight:700;color:#3a6072}
.scorebox code{background:#dde8ee;border-radius:3px;padding:0 4px;font-size:.72rem;color:#294a58;font-family:'Courier New',monospace}
.scorebox .flag{display:block;margin-top:.5rem;font-size:.73rem;color:#8a3a2a;background:#fbeee9;border:1px solid #ecc9bd;border-radius:6px;padding:.35rem .55rem}
.scorebox .flag b{color:#a5341f}
.gbox{background:#fff;border-radius:12px;box-shadow:0 1px 4px rgba(0,0,0,.04);padding:1.1rem 1.3rem;margin-bottom:1.2rem;border-left:4px solid #4a7c8a}
.gbox h2{font-size:1rem;color:#1f4b37;font-weight:800;margin-bottom:.5rem}
.gbox p{font-size:.82rem;line-height:1.5;color:#4a4a4a;margin-bottom:.5rem}
.gbox code{background:#eef3f5;border-radius:3px;padding:0 4px;font-size:.75rem;color:#294a58;font-family:'Courier New',monospace}
.gbox .rec{background:#f0f7f1;border:1px solid #cfe6d2;border-radius:8px;padding:.6rem .8rem;font-size:.8rem;color:#2f5e39;margin-top:.5rem}
.foot{font-size:.72rem;color:#8a8a8a;text-align:center;margin-top:2rem}
</style>
</head>
<body>
<div class="topbar">
<h1><img src="assets/img/geograsim_t.svg" alt="GeoGraSim"> Lehrer View · Parameter-Überblick Schülerleistung</h1>
<a class="btn-sm" href="admin-sitemap.html">← Admin-Übersicht</a>
<a class="btn-sm" href="admin-modules.html">Module</a>
<a class="btn-sm" href="teacher.html">Lehreransicht</a>
</div>
<div class="wrap">
<div class="intro">
<h2>Wozu diese Seite?</h2>
<p>Referenz für die <b>Verkabelung</b> jeder Simulation. Pro Sim siehst du die Tabelle aus dem Lehrer-Menüpunkt
<b>Ergebnisse&nbsp;/&nbsp;Aktuell</b> — dort erscheint jede Schüler:in mit den relevanten Leistungsdaten der laufenden Sim.
Die Tabellen hier sind <b>1:1 dieselbe Render-Logik</b> (<code>LIVE_PRIMARY_FIELDS</code> + Formatter aus <code>teacher.html</code>), nur mit <b>Testdaten</b>, damit du sie vergleichen kannst.</p>
<div class="wiring">
<span class="wire-chip"><b>Spiel</b><code>window.GGS_LIVE_STATE()</code> liefert die State-Keys</span>
<span class="wire-chip"><b>Lehrer-Backend</b> → Spalte + Formatter je Key</span>
<span class="wire-chip"><b>Schülerinfo</b> → Name · Avatar · Spielzeit</span>
</div>
<p style="font-size:.78rem;margin-top:.5rem">Unter jedem Spaltentitel steht der <b>State-Key</b> (der Parameter, den das Spiel senden muss). So ist ablesbar, welcher Wert aus dem Spiel welche Spalte speist.</p>
<div class="legend">
<span><span class="sw" style="background:#d9eddc"></span> <b>Grün = nach Abschluss noch relevant</b> — Ergebnis, Fortschritt (auch bei Abbruch) oder Identität der Aufgabe. Sollte in die Ergebnis-/Verlaufsansicht übernommen werden.</span>
<span><span class="sw" style="background:#f7f6f3"></span> <b>Grau = nur live</b> — momentaner Zustand, der sich laufend ändert oder zurücksetzt. Nach Abschluss ohne Aussagekraft.</span>
</div>
</div>
<div class="gbox">
<h2>So entstehen Prozent &amp; Sterne — und warum das heute uneinheitlich ist</h2>
<p><b>Der Zeilen-Prozentwert</b> in „Ergebnisse/Aktuell" entsteht in <code>live.php · extractScore</code>: liefert die Sim einen eigenen <b>0100-Score</b>, wird der genommen; sonst aus den Sternen: <code>% = Sterne / 5 × 100</code>.</p>
<p><b>Die Sterne</b> berechnet jede App am Ende selbst — aus unterschiedlichen Kennzahlen und Schwellen (je Sim unten dokumentiert). Deshalb bedeuten 4★ heute nicht überall dasselbe.</p>
<p><b>Uneinheitlichkeiten:</b> Busfahrt · Fluggesellschaft · Kofferdetektiv liefern nur <b>03 Sterne</b> statt 05. Busfahrt/Fluggesellschaft senden als <code>score</code> rohe Punkte (nicht 0100 → Zeilen-% verzerrt). Weltküche wertet <b>pro Gericht</b> statt pro Durchgang. <b>Logistik überträgt weder Sterne noch Score</b>. Ein echter 0100-Live-Score existiert nur bei Klima, Fluss, Heli, Kofferdetektiv, Tourismusregion (abgeleitet Tourismustal).</p>
<div class="rec"><b>Beschlossen:</b> <b>Sterne entfallen.</b> Jede App liefert genau <b>eine Funktion → Leistungs-% (0100)</b> als Wahrheit, überall gezeigt als Prozent + Balken in der 5-stufigen Farbskala. Auch Fördern / Begleiten / Läuft gut / Fordern leiten sich aus diesem % ab.</div>
</div>
<div id="preview"></div>
<div class="toc" id="toc"></div>
<div id="content"></div>
<p class="foot">Alle Werte sind Beispieldaten zur Ansicht. Render-Logik gespiegelt aus <code>teacher.html · renderLive()</code>. Grün/Grau ist Atlas' Vorschlag — bitte prüfen &amp; korrigieren.</p>
</div>
<script>
/* ============================================================
1) Formatter & Helfer — VERBATIM aus teacher.html gespiegelt,
damit die Zellen exakt wie in der echten Ansicht rendern.
============================================================ */
function esc(s){ var d=document.createElement('div'); d.textContent=s||''; return d.innerHTML; }
function _fmtSeconds(s){ s=Math.max(0,Math.round(s||0)); var mm=Math.floor(s/60), ss=s%60; return mm+':'+(ss<10?'0':'')+ss; }
function _fmtSince(sec){ return sec<60 ? sec+' s' : Math.floor(sec/60)+' min'; }
var LIVE_VALUE_LABELS = {
screen:{ start:'Auswahl', play:'Spielt', end:'Endscreen' },
difficulty:{ easy:'Einfach', expert:'Profi' },
stepType:{ pick:'Karte wählen', sentence:'Lückentext', multichoice:'4 Antworten', animation:'Animation' },
health:{
good:'<span style="color:#5a8a5e">● gut</span>',
ok:'<span style="color:#c9913a">● ok</span>',
struggling:'<span style="color:#c07a6b">● stockt</span>',
struggle:'<span style="color:#c07a6b">● stockt</span>',
},
};
function _liveBar(val, min, max, goodWhen, dec){
if (val==null || isNaN(val)) return '<span style="color:#9a9a9a">—</span>';
var span=(max-min)||1;
var pct=Math.max(0,Math.min(100,((val-min)/span)*100));
var c;
if (goodWhen==='low'){ c=pct<=33?'#5a8a5e':pct<=66?'#c9913a':'#c07a6b'; }
else if (goodWhen==='mid'){ c='#7a7a7a'; }
else { c=pct>=60?'#5a8a5e':pct>=30?'#c9913a':'#c07a6b'; }
var disp=(dec!=null)?Number(val).toFixed(dec):Math.round(val);
return '<span style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums">'
+ '<span aria-hidden="true" style="display:inline-block;width:42px;height:7px;border-radius:99px;background:rgba(0,0,0,.08);overflow:hidden">'
+ '<span style="display:block;height:100%;width:'+pct.toFixed(0)+'%;background:'+c+'"></span></span>'
+ '<span style="color:'+c+';font-weight:700;font-size:.8rem;min-width:1.6em;text-align:right">'+disp+'</span></span>';
}
function _liveBarColor(pct, goodWhen){
pct=Math.max(0,Math.min(100,pct||0));
if (goodWhen==='low') return pct<=33?'#5a8a5e':pct<=66?'#c9913a':'#c07a6b';
if (goodWhen==='mid') return '#9a9a9a';
return pct>=60?'#5a8a5e':pct>=30?'#c9913a':'#c07a6b';
}
function _liveMiniBar(pct, color){
pct=Math.max(0,Math.min(100,pct||0));
return '<div style="margin-top:3px;height:3px;border-radius:99px;background:rgba(0,0,0,.06);overflow:hidden">'
+ '<div style="height:100%;width:'+pct.toFixed(0)+'%;background:'+color+'"></div></div>';
}
function _liveNeedsHelp(simId, s){
if (!s || typeof s!=='object') return false;
switch (simId){
case 'heli': return (s.planAttempts||0) >= 10;
case 'klima': case 'klima-3d': return (s.budget||0) < 0;
case 'weltkueche': return (s.mistakesInDish||0) >= 12;
case 'farmer': return (s.cash==null?1000:s.cash) < 0 || ((s.attempts||0) >= 6 && s.hit_rate != null && s.hit_rate < 30);
case 'logistik': return (s.balance==null?1000:s.balance) < 0;
case 'eu-werkstatt': return (s.attemptsThisStep||0) >= 6 || (s.reshufflesThisStep||0) >= 5;
case 'energiemanager': return (s.blackouts||0) >= 2 || (s.phase==='done' && (s.balanced||0) < 4);
case 'tourismustal': return (s.money==null?1:s.money) < 0 || (s.lossStreak||0) >= 8;
case 'tourismusregion': return (s.budget==null?1:s.budget) < -400 || (s.env==null?100:s.env) < 30;
}
return false;
}
function _liveMood(simId, s){
if (!s) return null;
if (s.health) return ({good:'good',ok:'good',warning:'mid',bad:'bad',critical:'bad'})[s.health] || 'mid';
if (_liveNeedsHelp(simId, s)) return 'bad';
switch (simId){
case 'weltkueche': return (s.mistakesInDish||0)<=2?'good':(s.mistakesInDish>=8?'bad':'mid');
case 'farmer': return (s.hit_rate==null)?'mid':(s.hit_rate>=60?'good':s.hit_rate>=30?'mid':'bad');
case 'klima': case 'klima-3d': return (typeof s.score==='number')?(s.score>=65?'good':s.score>=40?'mid':'bad'):(((s.floodedPct==null?100:s.floodedPct)<25 && (s.budget||0)>=0)?'good':'mid');
case 'busfahrt': case 'fluggesellschaft': return (s.budget==null?0:s.budget)>=5000?'good':(s.budget<0?'bad':'mid');
case 'tourismustal': return (s.money==null?0:s.money)>=4000?'good':(s.money<0?'bad':'mid');
case 'heli': return (s.totalStars||0)>=4?'good':'mid';
case 'fluss': case 'kofferdetektiv': case 'sonnensystem': return s.score!=null?(s.score>=70?'good':s.score>=40?'mid':'bad'):'mid';
}
return 'mid';
}
function _liveMoodBadge(simId, s){
var m=_liveMood(simId,s);
if (!m) return '<span style="color:#8a8a8a">—</span>';
var map={ good:['#5a8a5e','Stark'], mid:['#c9913a','Solide'], bad:['#c07a6b','Viele Fehler'] };
var x=map[m]||map.mid;
return '<span style="display:inline-flex;align-items:center;gap:.3rem">'
+ '<span style="width:8px;height:8px;border-radius:50%;background:'+x[0]+';display:inline-block"></span>'
+ '<span style="font-size:.66rem;color:'+x[0]+';font-weight:700">'+x[1]+'</span></span>';
}
function _liveClicks(simId, s){
if (!s) return null;
if (typeof s.correctClicks==='number' || typeof s.wrongClicks==='number')
return { correct:s.correctClicks||0, wrong:s.wrongClicks||0 };
switch (simId){
case 'heli': if (s.planTotal) return { correct:s.planCorrect||0, wrong:Math.max(0,s.planTotal-(s.planCorrect||0)) }; break;
}
return null;
}
function _liveQualityBar(correct, wrong){
correct=Math.max(0,correct||0); wrong=Math.max(0,wrong||0);
var tot=correct+wrong;
if (!tot) return '<span style="color:#8a8a8a">—</span>';
var gp=Math.round(correct/tot*100), rp=100-gp;
var c=gp>=80?'#5a8a5e':gp>=55?'#c9913a':'#c07a6b';
return '<span title="'+correct+' richtig · '+wrong+' Fehler ('+gp+' % richtig)" style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums">'
+ '<span aria-hidden="true" style="display:inline-flex;width:56px;height:9px;border-radius:99px;overflow:hidden;background:#ece8dc">'
+ '<span style="width:'+gp+'%;background:#5a8a5e"></span><span style="width:'+rp+'%;background:#d98b7a"></span></span>'
+ '<span style="font-size:.72rem;font-weight:700;color:'+c+';min-width:2.3em;text-align:right">'+gp+' %</span></span>';
}
function _liveQualityFromState(simId, s){
var cw=_liveClicks(simId,s);
return cw ? _liveQualityBar(cw.correct,cw.wrong) : '<span style="color:#8a8a8a">—</span>';
}
function _liveFmt(v, unit, fieldDef, row){
if (fieldDef && typeof fieldDef.fmt==='function'){
try { var f=fieldDef.fmt(v,row); return (f===undefined||f===null||f==='')?'—':f; } catch(_){}
}
if (v===undefined||v===null||v==='') return '—';
if (typeof v==='object') return JSON.stringify(v).slice(0,30);
if (typeof v==='boolean') return v?'✓':'';
return v + (unit?' <span style="color:#8a8a8a;font-weight:400">'+unit+'</span>':'');
}
/* ============================================================
2) Spaltenschema — VERBATIM aus teacher.html (LIVE_PRIMARY_FIELDS)
============================================================ */
var LIVE_PRIMARY_FIELDS = {
farmer: [
{k:'year', label:'Jahr', bar:{kind:'abs',max:10,good:'mid'}, fmt:function(v){ return (v||0) + '<span style="color:#8a8a8a"> / 10</span>'; }},
{k:'market_de', label:'Markt', align:'left', fmt:function(v){ return v ? '🌍 '+esc(v) : '<span style="color:#8a8a8a">—</span>'; }},
{k:'cash', label:'Konto', bar:{kind:'rel',good:'signed'}, fmt:function(v){ if (v==null) return '<span style="color:#9a9a9a">—</span>'; var color=v<0?'#c0392b':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>'; }},
{k:'hit_rate', label:'Trefferquote', unit:'%', bar:{kind:'abs',max:100,good:'high'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'crops_used', label:'Sorten', bar:{kind:'abs',max:10,good:'high'}, fmt:function(v){ return (v||0) + '<span style="color:#8a8a8a"> / 10</span>'; }},
{k:'countries', label:'Länder', bar:{kind:'rel',good:'high'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'fields', label:'Felder', bar:{kind:'rel',good:'mid'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'attempts', label:'Versuche',bar:{kind:'rel',good:'mid'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'co2_kg', label:'CO₂', unit:'kg', bar:{kind:'rel',good:'low'}, fmt:function(v){ if (v==null) return '<span style="color:#9a9a9a">—</span>'; return '<span style="font-variant-numeric:tabular-nums">'+Number(Math.round(v)).toLocaleString('de-AT')+'</span>'; }},
{k:'diseases', label:'Krankheiten', align:'center', bar:{kind:'abs',max:5,good:'low'}, fmt:function(v){ if (v==null) return '<span style="color:#9a9a9a">—</span>'; var color=(v>0)?'#c07a6b':'#8a8a8a'; return '<span style="color:'+color+';font-weight:'+(v>0?'700':'400')+'">'+v+'</span>'; }},
],
'tourismusregion': [
{k:'mapId', label:'Karte', align:'left', fmt:function(v){ return {talschleife:'🏞 Talschleife', seerunde:'🚣 Seerunde', serpentinen:'⛰ Serpentinen', kreuzung:'✚ Kreuzung', passstrasse:'🛣 Passstraße'}[v] || v || '—'; }},
{k:'wave', label:'Welle', fmt:function(v, row){ var s=row.state||{}; if (v===undefined) return '—'; var status=s.waveStatus==='pause'?' <span style="color:#8a8a8a">⏸ Baupause</span>':''; return v+'<span style="color:#8a8a8a"> / '+(s.waveTotal||8)+'</span>'+status; }},
{k:'season', label:'Saison', align:'center'},
{k:'budget', label:'Budget', fmt:function(v){ if (v===undefined) return '—'; var color=v<0?'#c0392b':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>'; }},
{k:'revenue', label:'Wertsch.', unit:'€'},
{k:'avgStars', label:'Ø ★', fmt:function(v){ if (v===undefined) return '—'; var color=v>=3.5?'#5a8a5e':v>=2.5?'#c9913a':'#c07a6b'; return '<span style="color:'+color+';font-weight:700">'+v.toFixed(2)+'</span>'; }},
{k:'env', label:'Umwelt', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'visitors', label:'Gäste'},
{k:'goalsDone', label:'Ziele', fmt:function(v, row){ var s=row.state||{}; if (v===undefined||!s.goalsTotal) return '—'; var color=v===s.goalsTotal?'#5a8a5e':v>=2?'#c9913a':'#8a8a8a'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+s.goalsTotal+'</span>'; }},
],
'tourismustal': [
{k:'scene', label:'Szene', align:'left', fmt:function(v){ return {pionierzeit:'⛏️ 1975', boom:'📈 Boom 2000', klimawandel:'🌡️ 2035'}[v] || v || '—'; }},
{k:'season', label:'Saison', align:'center'},
{k:'year', label:'Jahr'},
{k:'money', label:'Konto', fmt:function(v){ if (v===undefined) return '—'; var color=v<0?'#c0392b':v<4000?'#c9913a':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>'; }},
{k:'guestsToday', label:'Gäste'},
{k:'satisfaction', label:'Zufr.', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'nature', label:'Natur', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'goalsDone', label:'Ziele', fmt:function(v, row){ var s=row.state||{}; if (v===undefined||!s.goalsTotal) return '—'; var color=v===s.goalsTotal?'#5a8a5e':v>=2?'#c9913a':'#8a8a8a'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+s.goalsTotal+'</span>'; }},
{k:'buildings', label:'Geb.'},
{k:'bottleneck', label:'Engpass', align:'left', fmt:function(v){ return v ? '<span style="color:#c07a6b">⚠️ '+v+'</span>' : '<span style="color:#8a8a8a">—</span>'; }},
],
klima: [
{k:'level', label:'Stufe'},
{k:'year', label:'Jahr'},
{k:'co2', label:'CO₂', unit:'ppm', fmt:function(v){ return _liveBar(v,280,700,'low'); }},
{k:'temp', label:'Temp', unit:'°C', fmt:function(v){ return _liveBar(v,14,20,'low',1); }},
{k:'floodedPct', label:'Geflutet', unit:'%', fmt:function(v){ if (v==null||isNaN(v)) return '<span style="color:#9a9a9a">—</span>'; var c=v<25?'#5a8a5e':v<=40?'#c9913a':'#c07a6b'; var pct=Math.max(0,Math.min(100,v)); return '<span style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums"><span aria-hidden="true" style="display:inline-block;width:42px;height:7px;border-radius:99px;background:rgba(0,0,0,.08);overflow:hidden"><span style="display:block;height:100%;width:'+pct.toFixed(0)+'%;background:'+c+'"></span></span><span style="color:'+c+';font-weight:700;font-size:.8rem;min-width:1.6em;text-align:right">'+Math.round(v)+'</span></span>'; }},
{k:'budget', label:'Budget', unit:'Mio €', fmt:function(v){ return _liveBar(v,-500,3000,'high'); }},
{k:'population', label:'Bev.', fmt:function(v, row){ var sp=(((row&&row.state)||{}).level>=3)?7000:6000; return _liveBar(v,sp*0.3,sp*1.6,'high'); }},
{k:'measures', label:'Maßn.'},
{k:'speed', label:'Speed', unit:'×'},
],
'sonnensystem': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'screen', label:'Modus', fmt:function(v){ return v==='play'?'📚 Aufgabe':'🚀 Erkunden'; }},
{k:'taskTitle', label:'Aufgabe', align:'left', fmt:function(v, row){ var s=row.state||{}; if (s.screen!=='play'||!v) return '<span style="color:#8a8a8a">—</span>'; return v+' <span style="color:#8a8a8a;font-weight:400">('+s.taskIndex+'/'+s.taskTotal+')</span>'; }},
{k:'completed', label:'Gelöst', fmt:function(v, row){ var s=row.state||{}; return s.taskTotal ? v+' <span style="color:#8a8a8a">/ '+s.taskTotal+'</span>' : '—'; }},
{k:'progressPct', label:'Quote', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'simDate', label:'Sim-Datum', align:'center'},
{k:'achievementsUnlocked', label:'Erfolge', fmt:function(v, row){ var s=row.state||{}; if (v===undefined) return '—'; return v+' <span style="color:#8a8a8a">/ '+(s.achievementsTotal||6)+'</span>'; }},
],
'energiemanager': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'setTitle', label:'Set', align:'left', fmt:function(v, row){ var s=row.state||{}; var icon=s.setIcon?'<span style="margin-right:.25rem">'+s.setIcon+'</span>':''; var idxBadge=(s.setIndex&&s.setTotal)?' <span style="color:#8a8a8a;font-weight:400">'+s.setIndex+'/'+s.setTotal+'</span>':''; return v ? icon+v+idxBadge : '—'; }},
{k:'day', label:'Tag', fmt:function(v, row){ var s=row.state||{}; return s.dayTotal ? v+' <span style="color:#8a8a8a">/ '+s.dayTotal+'</span>' : '—'; }},
{k:'block', label:'Block', fmt:function(v, row){ var s=row.state||{}; if (s.phase==='blackout') return '<span style="color:#d8453a;font-weight:700">🔌 neu planen</span>'; if (s.phase==='plan') return '<span style="color:#8a8a8a">plant</span>'; return s.blockTotal ? (v+1)+' <span style="color:#8a8a8a">/ '+s.blockTotal+'</span>' : '—'; }},
{k:'blackouts', label:'🔌', align:'center', fmt:function(v){ if (!v) return '<span style="color:#8a8a8a"></span>'; var c=v>=2?'#d8453a':'#c9913a'; return '<span style="color:'+c+';font-weight:700">'+v+'</span>'; }},
{k:'balanced', label:'Tag ausg.', fmt:function(v, row){ var s=row.state||{}; if (s.phase!=='done') return '<span style="color:#8a8a8a">—</span>'; var color=v===s.blockTotal?'#5a8a5e':v>=4?'#c9913a':'#c07a6b'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+s.blockTotal+'</span>'; }},
{k:'setBlocksBalanced', label:'Set ausg.', fmt:function(v, row){ var s=row.state||{}; var max=(s.setTotal&&s.dayTotal&&s.blockTotal)?s.dayTotal*s.blockTotal:0; if (!max) return '—'; var color=v>=max*0.87?'#5a8a5e':v>=max*0.5?'#c9913a':'#c07a6b'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+max+'</span>'; }},
{k:'upperLake', label:'⬆ See', unit:'GWh'},
{k:'lowerLake', label:'⬇ See', unit:'GWh'},
],
'eu-werkstatt': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'scenarioTitle', label:'Szenario', align:'left', fmt:function(v, row){ var s=row.state||{}; var icon=s.scenarioIcon?'<span style="margin-right:.25rem">'+s.scenarioIcon+'</span>':''; return v ? icon+v : '<span style="color:#8a8a8a">Szenario-Auswahl</span>'; }},
{k:'step', label:'Schritt', fmt:function(v, row){ var s=row.state||{}; if (!s.stepsTotal) return '—'; return v+' <span style="color:#8a8a8a">/ '+s.stepsTotal+'</span>'; }},
{k:'difficulty', label:'Modus', fmt:function(v){ return LIVE_VALUE_LABELS.difficulty[v] || '—'; }},
{k:'firstTryHits', label:'Treffer'},
{k:'muelleimerCount', label:'Mülleimer', fmt:function(v){ if (v===undefined||v===null) return '—'; var color=v===0?'#5a8a5e':v>=3?'#c07a6b':'#c9913a'; return '<span style="color:'+color+';font-weight:700">'+v+'</span>'; }},
{k:'reshuffles', label:'🔄 Probier', align:'center', fmt:function(v){ if (!v) return '<span style="color:#8a8a8a"></span>'; var color=v>=5?'#c07a6b':v>=2?'#c9913a':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+v+'</span>'; }},
{k:'secondsElapsed', label:'Dauer', fmt:function(v){ return v ? _fmtSeconds(v) : '—'; }},
{k:'achievementsUnlocked', label:'Erfolge', fmt:function(v, row){ var s=row.state||{}; if (v===undefined||v===null) return '—'; return v+' <span style="color:#8a8a8a">/ '+(s.achievementsTotal||6)+'</span>'; }},
],
'weltkueche': [
{k:'dishName', label:'Gericht', align:'left', fmt:function(v){ return v ? '<b>'+esc(v)+'</b>' : '<span style="color:#8a8a8a">Auswahl</span>'; }},
{k:'completed', label:'Gerichte', align:'center', fmt:function(v, row){ var s=row.state||{}; var t=s.completedTotal||0; if (!t) return '<span style="color:#8a8a8a">—</span>'; var done=v||0; var c=done>=t?'#5a8a5e':'#3d5165'; return '<b style="color:'+c+'">'+done+'</b><span style="color:#8a8a8a"> / '+t+'</span>'; }},
{k:'ingSolved', label:'Zutaten akt.', fmt:function(v, row){ var s=row.state||{}; var t=s.ingTotal||0; if (!s.dishName||!t) return '<span style="color:#8a8a8a">—</span>'; var done=v||0; var pct=done/t; var c=pct>=1?'#5a8a5e':pct>=.5?'#c9913a':'#8a7a4a'; return '<span style="color:'+c+';font-weight:700">'+done+' <span style="color:#8a8a8a;font-weight:400">/ '+t+'</span></span>'; }},
{k:'__quality', label:'Qualität', align:'left', fmt:function(v, row){ return _liveQualityFromState(row.module_id, row.state); }},
{k:'mistakesInDish', label:'Fehler (akt.)', align:'center', fmt:function(v){ var n=v||0; var c=n>=12?'#c07a6b':n>=5?'#c9913a':'#5a8a5e'; return '<span style="color:'+c+';font-weight:700">'+n+'</span>'; }},
{k:'__lage', label:'Lage', align:'center', fmt:function(v, row){ return _liveMoodBadge(row.module_id, row.state); }},
],
'busfahrt': [
{k:'levelId', label:'Stufe', align:'center'},
{k:'orderIndex', label:'Auftrag', fmt:function(v, row){ var s=row.state||{}; return s.totalOrders ? (v+1)+' / '+s.totalOrders : '—'; }},
{k:'budget', label:'Budget', unit:'€', fmt:function(v){ if (v==null) return '—'; var c=v>=5000?'#5a8a5e':v>=0?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+v.toLocaleString('de-AT')+' €</span>'; }},
{k:'wonOrders', label:'Aufträge ✓'},
{k:'lastCityTitle', label:'Letzte Stadt', align:'left'},
{k:'lastMargin', label:'Letzte Marge', unit:'€'},
{k:'quizFirstTry', label:'🧠 1.-Versuch', fmt:function(v, row){ var s=row.state||{}; var t=s.orderIndex||0; return t ? v+' / '+t : '—'; }},
],
'fluggesellschaft': [
{k:'levelId', label:'Stufe', align:'center'},
{k:'orderIndex', label:'Flug', fmt:function(v, row){ var s=row.state||{}; return s.totalOrders ? (v+1)+' / '+s.totalOrders : '—'; }},
{k:'budget', label:'Budget', unit:'€', fmt:function(v){ if (v==null) return '—'; var c=v>=8000?'#5a8a5e':v>=0?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+v.toLocaleString('de-AT')+' €</span>'; }},
{k:'wonOrders', label:'Flüge ✓'},
{k:'co2Kg', label:'🌍 CO₂', fmt:function(v){ return v==null ? '—' : (v/1000).toFixed(1)+' t'; }},
{k:'lastCityTitle', label:'Letztes Ziel', align:'left'},
{k:'lastTzDiff', label:'Zeitzone', fmt:function(v){ if (v==null) return '—'; return (v>0?'+':'')+v+' h'; }},
{k:'quizFirstTry', label:'🧠 1.-Versuch', fmt:function(v, row){ var s=row.state||{}; var t=s.orderIndex||0; return t ? v+' / '+t : '—'; }},
],
'heli': [
{k:'phase', label:'Phase', align:'center', fmt:function(v){ var m={mission:'📋 Auftragswahl', plan:'🗺️ Routenplan', briefing:'📋 Briefing', start:'🛫 Start', flight:'✈️ Flug', approach:'🎯 Anflug', landing:'🛬 Landung', results:'🏁 Ende'}; return m[v] || (v ? v : '—'); }},
{k:'missionId', label:'Auftrag', align:'left', fmt:function(v, row){ var s=row.state||{}; var title=s.missionTitle||''; return v ? '<b>'+v+'</b>'+(title?' <span style="color:#8a8a8a;font-weight:400">'+title+'</span>':'') : '—'; }},
{k:'planIndex', label:'🗺️ Wegpunkt', fmt:function(v, row){ var s=row.state||{}; var t=s.planTotal||0; if (!t) return '—'; var done=(v||0); var pct=done/t; var c=pct>=1?'#5a8a5e':pct>=.5?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+done+' / '+t+'</span>'; }},
{k:'planCorrect', label:'🎯 Treffer', fmt:function(v, row){ var s=row.state||{}; var t=s.planTotal||0; if (!t) return '—'; var done=(v||0); var pct=done/t; var c=pct>=.9?'#5a8a5e':pct>=.5?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+done+' / '+t+'</span>'; }},
{k:'planAttempts', label:'❌ Fehlkl.', align:'center', fmt:function(v){ var n=v||0; var c=n<=2?'#5a8a5e':n<=8?'#c9913a':'#c07a6b'; return n===0 ? '<span style="color:#5a8a5e;font-weight:700">0</span>' : '<span style="color:'+c+';font-weight:700">'+n+'</span>'; }},
{k:'startScore', label:'🛫 Start', fmt:function(v){ var n=Math.max(0,Math.min(5,v||0)); var c=n>=4?'#5a8a5e':n>=2?'#c9913a':'#c07a6b'; return v==null ? '—' : '<span style="color:'+c+';font-weight:700">'+'★'.repeat(n)+'<span style="color:#ccc">'+'★'.repeat(5-n)+'</span></span>'; }},
{k:'landingScore', label:'🛬 Landung', fmt:function(v){ var n=Math.max(0,Math.min(5,v||0)); var c=n>=4?'#5a8a5e':n>=2?'#c9913a':'#c07a6b'; return v==null ? '—' : '<span style="color:'+c+';font-weight:700">'+'★'.repeat(n)+'<span style="color:#ccc">'+'★'.repeat(5-n)+'</span></span>'; }},
{k:'totalStars', label:'⭐ Gesamt', fmt:function(v){ var n=Math.max(0,Math.min(5,v||0)); var c=n>=4?'#5a8a5e':n>=2?'#c9913a':'#c07a6b'; return v==null ? '—' : '<span style="color:'+c+';font-weight:800;font-size:.82rem">'+'★'.repeat(n)+'<span style="color:#ccc">'+'★'.repeat(5-n)+'</span></span>'; }},
{k:'failed', label:'Status', align:'center', fmt:function(v, row){ var s=row.state||{}; if (v===true) return '<span style="color:#c07a6b;font-weight:700">⚠ Chefpilot</span>'; if (s.phase==='results') return '<span style="color:#5a8a5e;font-weight:700">✓ Erfolg</span>'; if (s.phase==='mission'||!s.phase){ if (s.lastResult) return s.lastFailed ? '<span style="color:#c07a6b;font-weight:600" title="Werte = letzter Einsatz">⚠ letzter Einsatz</span>' : '<span style="color:#5a8a5e;font-weight:600" title="Werte = letzter Einsatz">✓ letzter Einsatz</span>'; return '<span style="color:#8a8a8a">wählt Auftrag</span>'; } return '<span style="color:#4a7c8a;font-weight:600">🟢 unterwegs</span>'; }},
],
'fluss': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'levelId', label:'Stufe', align:'center'},
{k:'year', label:'Jahr', fmt:function(v, row){ var s=row.state||{}; var max=100; return s.progressPct!=null ? (v||0)+' <span style="color:#8a8a8a">/ '+max+'</span>' : (v||'—'); }},
{k:'score', label:'🏅 Score', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'progressPct', label:'Fortschritt', fmt:function(v){ return _liveBar(v,0,100,'mid'); }},
{k:'budget', label:'Budget', unit:'Mio €', fmt:function(v){ if (v==null) return '—'; var c=v>=20?'#5a8a5e':v>=0?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+v+'</span>'; }},
{k:'biodiv', label:'🌳 Biodiv.', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'economy', label:'💰 Wirtsch.', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'flood', label:'🛡️ Hochw.-Schutz', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'settlementsLost', label:'🏚️ Verlust', align:'center', fmt:function(v){ if (!v) return '<span style="color:#5a8a5e">0</span>'; var c=v>=3?'#c07a6b':'#c9913a'; return '<span style="color:'+c+';font-weight:700">'+v+'</span>'; }},
],
'kofferdetektiv': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'phase', label:'Phase'},
{k:'progressPct', label:'Fortschritt', fmt:function(v){ return _liveBar(v,0,100,'mid'); }},
{k:'score', label:'🏅 Score', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'cases_solved', label:'Gelöst', align:'center', fmt:function(v, row){ var s=row.state||{}; var t=s.cases_total||0; return t ? v+' <span style="color:#8a8a8a">/ '+t+'</span>' : (v||'—'); }},
{k:'case_iso2', label:'🎯 Ziel', align:'left', fmt:function(v, row){ var s=row.state||{}; if (!v) return '—'; var iso=String(v).toUpperCase(); if (s.case_country) return '<span style="font-weight:600;color:#5a8a5e">'+esc(s.case_country)+'</span>'; return '<span style="font-weight:600;color:#b04a3e" title="Antwort verdeckt — Schüler:in sucht noch">'+iso+' (suchend)</span>'; }},
{k:'hints_revealed', label:'Hinweise', align:'center'},
{k:'wrong_guesses_total', label:'❌ Fehler', fmt:function(v){ return _liveBar(v,0,10,'low'); }},
{k:'time_left_s', label:'⏱ Rest', align:'center', fmt:function(v){ if (v==null) return '<span style="color:#8a8a8a">∞</span>'; var c=v<=30?'#c07a6b':v<=120?'#c9913a':'#5a8a5e'; return '<span style="color:'+c+';font-weight:700">'+_fmtSeconds(v)+'</span>'; }},
{k:'airport', label:'✈ Flughafen', align:'left'},
],
'logistik': [
{k:'levelId', label:'Stufe', align:'center'},
{k:'balance', label:'Budget', fmt:function(v){ if (v==null) return '—'; var c=v>=5000?'#5a8a5e':v>=0?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>'; }},
{k:'ordersDelivered', label:'Geliefert', align:'center', fmt:function(v, row){ var s=row.state||{}; var t=s.totalOrders||0; var done=v||0; var c=done>0?'#5a8a5e':'#8a8a8a'; return '<b style="color:'+c+'">'+done+'</b>'+(t?'<span style="color:#8a8a8a"> / '+t+'</span>':''); }},
{k:'ordersInFlight', label:'Unterwegs', align:'center'},
{k:'ordersOpen', label:'Offen', align:'center'},
{k:'ordersLate', label:'Verspätet', align:'center', fmt:function(v){ var n=v||0; return n>0 ? '<span style="color:#c07a6b;font-weight:700">'+n+'</span>' : '<span style="color:#5a8a5e">0</span>'; }},
{k:'progressPct', label:'Fortschritt', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
],
};
/* ============================================================
3) Reihenfolge, Titel/Icons + Grün-Bewertung je Sim
GREEN = Keys, die NACH Abschluss noch relevant sind
(Ergebnis · Fortschritt/Abbruch · Aufgaben-Identität).
Alles andere = nur live.
============================================================ */
var SIM_META = [
{id:'farmer', icon:'🌾', title:'Farmer'},
{id:'klima', icon:'🌍', title:'Klimawächter'},
{id:'fluss', icon:'🌊', title:'Flussmanagement'},
{id:'energiemanager', icon:'⚡', title:'Energiemanager'},
{id:'eu-werkstatt', icon:'🇪🇺', title:'EU-Werkstatt'},
{id:'weltkueche', icon:'🍽️', title:'Weltküche'},
{id:'busfahrt', icon:'🚌', title:'Busfahrt'},
{id:'fluggesellschaft', icon:'✈️', title:'Fluggesellschaft'},
{id:'heli', icon:'🚁', title:'Hubschrauber-Rettung'},
{id:'kofferdetektiv', icon:'🧳', title:'Kofferdetektiv'},
{id:'sonnensystem', icon:'🪐', title:'Sonnensystem'},
{id:'tourismustal', icon:'🏔️', title:'Tourismustal'},
{id:'tourismusregion', icon:'🗺️', title:'Tourismusregion'},
{id:'logistik', icon:'📦', title:'Logistik'},
];
var GREEN = {
farmer: ['year','cash','hit_rate','crops_used','countries','fields','attempts','co2_kg','diseases'],
klima: ['level','year','co2','temp','floodedPct','budget','population','measures'],
fluss: ['levelId','year','score','progressPct','budget','biodiv','economy','flood','settlementsLost'],
energiemanager: ['setTitle','day','blackouts','balanced','setBlocksBalanced'],
'eu-werkstatt': ['scenarioTitle','step','difficulty','firstTryHits','muelleimerCount','reshuffles','secondsElapsed','achievementsUnlocked'],
weltkueche: ['completed','__quality'],
busfahrt: ['levelId','orderIndex','budget','wonOrders','quizFirstTry'],
fluggesellschaft: ['levelId','orderIndex','budget','wonOrders','co2Kg','quizFirstTry'],
heli: ['missionId','planIndex','planCorrect','planAttempts','startScore','landingScore','totalStars','failed'],
kofferdetektiv: ['progressPct','score','cases_solved','hints_revealed','wrong_guesses_total'],
sonnensystem: ['completed','progressPct','achievementsUnlocked'],
tourismustal: ['scene','year','money','guestsToday','satisfaction','nature','goalsDone','buildings'],
tourismusregion: ['mapId','wave','budget','revenue','avgStars','env','goalsDone'],
logistik: ['levelId','balance','ordersDelivered','ordersLate','progressPct'],
};
// Kurzbegründung, warum ein Grau-Key nur live relevant ist.
var GREY_REASON = {
market_de:'aktueller Handelsmarkt — wechselt ständig',
season:'momentane Jahreszeit im Spiel',
visitors:'Gäste im aktuellen Moment',
guestsToday:'Gäste nur am heutigen Sim-Tag',
bottleneck:'aktueller Engpass — ändert sich laufend',
speed:'Simulationstempo (Bedien-Einstellung, keine Leistung)',
health:'Live-Ampel des Moments — das Ergebnis steckt in Score/Sternen',
screen:'was gerade offen ist (Aufgabe/Erkunden)',
taskTitle:'aktuell offene Aufgabe',
simDate:'gerade beobachtetes Datum',
block:'aktueller Zeitblock im Tag',
upperLake:'momentaner Speicherstand (oberer See)',
lowerLake:'momentaner Speicherstand (unterer See)',
dishName:'aktuell gekochtes Gericht',
ingSolved:'Zutaten im aktuellen Gericht (setzt sich je Gericht zurück)',
mistakesInDish:'Fehler nur im aktuellen Gericht (setzt sich zurück)',
__lage:'Live-Stimmung — Ergebnis steckt in Qualität/Gerichte',
lastCityTitle:'zuletzt angefahrenes Ziel',
lastMargin:'Marge des letzten Auftrags',
lastTzDiff:'Zeitzonendifferenz des letzten Ziels',
phase:'aktueller Spielabschnitt',
case_iso2:'aktuelles Zielland — bewusst verdeckt, nur live',
time_left_s:'Restzeit im laufenden Fall',
airport:'aktueller Standort',
ordersInFlight:'Aufträge gerade unterwegs',
ordersOpen:'offene Aufträge im Moment',
};
/* ============================================================
3b) Bewertung je Sim — wie Sterne UND Prozent zustandekommen.
Quelle: Sterne-Audit der Game-Files (Formeln/Schwellen 1:1).
============================================================ */
var SCORING = {
farmer: {
stars:'<b>05</b> aus <code>totalScore</code> (0100, gewichtet: Konto 25% · Trefferquote 20% · CO₂ 15% · Krankheiten 10% · Sorten 10% · Länder 10% · Klimazonen 10%). Schwellen: <code>≥85→5 · ≥70→4 · ≥50→3 · ≥30→2 · sonst 1</code>.',
pct:'Kein 0100-Live-Score → Zeilen-% = <code>Sterne/5×100</code> (im Ergebnis liegt zusätzlich <code>data.score</code>).',
},
klima: {
stars:'<b>05</b>: Basis <code>2★</code> bei Sieg, dann <code>+1</code> je Temp&lt;17° · Temp&lt;16,3° · Budget&gt;200 · (Flut&lt;20% &amp; Bev.&gt;90% Start). Deckel: Temp≥17°→max 3★, ≥18,5°→max 1★.',
pct:'Eigener 0100-Score = <code>TempScore(035)+FlutScore(025)+BudgetScore(020)+BevScore(020)</code>. Live-Score identisch → Zeilen-% = dieser Score.',
},
fluss: {
stars:'<b>05</b> aus Score: <code>≥85→5 · ≥70→4 · ≥55→3 · ≥35→2 · sonst 1</code>.',
pct:'Score 0100 = <code>Ø(Biodiv, Wirtschaft, Hochwasserschutz) 5×Siedlungsverluste</code>. Live-Score vorhanden → Zeilen-% = Score.',
},
energiemanager: {
stars:'<b>05</b> aus <code>Quote = ausgeglichene Blöcke / gesamt</code>: <code>≥1,0→5 · ≥0,87→4 · ≥0,66→3 · ≥0,50→2 · sonst 1</code>. Blackout-Deckel: 1×→max 3★, ≥2×→max 2★.',
pct:'Kein 0100-Live-Score → Zeilen-% = <code>Sterne/5×100</code>.',
},
'eu-werkstatt': {
stars:'<b>05</b> aus <code>firstTryRate = saubere Erstversuche / Schritte</code>: <code>≥1,0→5 · ≥0,85→4 · ≥0,65→3 · ≥0,40→2 · sonst 1</code>.',
pct:'Kein 0100-Live-Score → Zeilen-% = <code>Sterne/5×100</code>.',
},
weltkueche: {
stars:'<b>05 pro Gericht</b> aus Fehlern: <code>0→5 · ≤1→4 · ≤3→3 · ≤6→2 · sonst 1</code>.',
pct:'Live-<code>score</code> ist eine <b>rohe Punktezahl</b> (nicht 0100) → Zeilen-% aktuell unklar.',
flags:['Wird <b>pro Gericht</b> gesendet, nicht pro Durchgang.','„Fehler in %" (Fehler ÷ Aktionen) gehört genau hierher — vergleichbar statt roher, gericht-abhängiger Zahl.'],
},
busfahrt: {
stars:'<b>nur 03</b> aus <code>Ratio = Topo-Punkte / (Aufträge×3)</code>: <code>≥0,85→3 · ≥0,55→2 · ≥0,30→1 · sonst 0</code>.',
pct:'Gesendeter <code>score</code> = rohe Topo-Punkte (nicht 0100).',
flags:['03 statt 05 Sterne.','score ist keine 0100-Zahl → Zeilen-% verzerrt.'],
},
fluggesellschaft: {
stars:'<b>nur 03</b> aus <code>Ratio = Topo-Punkte / (Flüge×3)</code>: <code>≥0,85→3 · ≥0,55→2 · ≥0,30→1 · sonst 0</code>.',
pct:'Gesendeter <code>score</code> = rohe Topo-Punkte (nicht 0100).',
flags:['03 statt 05 Sterne.','score ist keine 0100-Zahl → Zeilen-% verzerrt.'],
},
heli: {
stars:'<b>05</b> = <code>round((PlanSterne + Start + Landung) / 3)</code>. PlanSterne aus Timing (Ø≥1,6→5 …) bzw. Klickgenauigkeit; Chefpilot-Übernahme → 0.',
pct:'Live-Score 0100 = <code>Sterne/5×100</code> (submit <code>score = Sterne×20</code>) → Zeilen-% = das.',
},
kofferdetektiv: {
stars:'<b>nur 03</b> aus <code>q = score100/100</code>: <code>≥0,7→3 · ≥0,45→2 · ≥0,2→1 · sonst 0</code>.',
pct:'<code>score100 = Punkte / (Fälle×100) × 100</code> (je Fall 100 P, 7/Hinweis, 15/Fehltipp, 20/Timeout). Live-Score = score100 → Zeilen-% = das.',
flags:['03 statt 05 Sterne.'],
},
sonnensystem: {
stars:'<b>15</b> aus <code>Quote = Erstversuch-richtig / gesamt</code>: <code>≥0,85→5 · ≥0,68→4 · ≥0,50→3 · ≥0,30→2 · sonst 1</code> (Minimum 1, nie 0).',
pct:'Kein 0100-Live-Score (nur <code>levelWon</code>+Fortschritt; <code>scoreQuote</code> im data) → Zeilen-% = <code>Sterne/5×100</code>.',
},
tourismustal: {
stars:'<b>05</b> = <code>min(5, erreichte Ziele + (Sieg?1:0))</code>.',
pct:'Score 0100 = <code>erreichte Ziele / gesamt × 100</code>; kein Live-0100 → Zeilen-% = data.score bzw. Sterne/5×100.',
},
tourismusregion: {
stars:'<b>05</b> = <code>round(Ø Besucher-Bewertung)</code> (avgStars 05).',
pct:'Score 0100 = gewichtet: <code>Bewertung 35% · Wertschöpfung 25% · Nachhaltigkeit 20% · Lenkung 10% · Finanzen 10%</code>. Live <code>avgStars</code> → Zeilen-% = Score.',
},
logistik: {
stars:'<b>05</b> aus Endkontostand: <code>≤0→0 · ≥16000→5 · ≥13000→4 · ≥10500→3 · ≥8500→2 · sonst 1</code>.',
pct:'Kein Score/Stern wird ans Backend gesendet.',
flags:['Sterne &amp; Score werden <b>NICHT übertragen</b> — im Lehrer-Cockpit erscheint für Logistik kein Stern/Prozent. Eigene Endpoints <code>/api/assessment</code> + <code>/api/logistik-sessions</code>.'],
},
};
/* ============================================================
3c) VORSCHLAG — einheitliches Leistungs-% (0100) je Sim,
5-stufige Farbskala, %+Balken-Komponente, Triage (%×Fleiß).
============================================================ */
var PROPOSED_PCT = {
farmer:'gewichteter 0100-Score (wie heute intern berechnet) — künftig auch LIVE senden.',
klima:'vorhandener 0100-Score (Temp+Flut+Budget+Bev.) übernehmen.',
fluss:'vorhandener Score Ø(Biodiv,Wirtschaft,Schutz) 5×Verluste übernehmen.',
energiemanager:'ausgeglichene Blöcke / (Tage×8) ×100 15 je Blackout.',
'eu-werkstatt':'saubere Erstversuche / Schritte ×100 5 je Mülleimer.',
weltkueche:'richtige / (richtige+falsche) ×100 (= 100 Fehlerquote).',
busfahrt:'Topo-Punkte / (Aufträge×3) ×100.',
fluggesellschaft:'Topo-Punkte / (Flüge×3) ×100.',
heli:'kontinuierlich aus PlanPct + Start + Landung (0100), nicht gerundete Sterne.',
kofferdetektiv:'vorhandener score100 (0100) übernehmen.',
sonnensystem:'Erstversuch-Quote ×100.',
tourismustal:'Ziele/gesamt ×100, gewichtet mit Zufriedenheit/Natur.',
tourismusregion:'vorhandener gewichteter 0100-Score übernehmen.',
logistik:'Kontostand→% (0 €→0, Zielbudget→100) — UND ans Backend senden.',
};
// 5-stufige Farbskala (= _tagTraffic).
// ≥90 Dunkelgrün · ≥80 Hellgrün · ≥60 Gelb · ≥50 Orange · <50 Rot
function pctColor(v){ v=Math.max(0,Math.min(100,v||0)); return v>=90?'#1f7a3d':v>=80?'#7cb342':v>=60?'#f9c33b':v>=50?'#ef8b2c':'#d8453a'; }
// Standard-Anzeige überall: Prozent in Ampelfarbe + schmaler Balken darunter (kompakt).
function pctBar(pct){
if (pct==null) return '<span style="color:#9a9a9a">—</span>';
var c=pctColor(pct);
return '<div style="width:40px">'
+ '<div style="font-weight:800;color:'+c+';font-size:.78rem;font-variant-numeric:tabular-nums;line-height:1">'+pct+'<span style="font-size:.6rem"> %</span></div>'
+ '<div style="margin-top:2px;height:4px;border-radius:99px;background:rgba(0,0,0,.07);overflow:hidden"><div style="height:100%;width:'+pct+'%;background:'+c+'"></div></div>'
+ '</div>';
}
// Triage aus Leistungs-% × Aktivität — Grenzen = Farbgrenzen, damit Feld ↔ Farbe stimmen.
// <50 Rot→Fördern · 5079 Orange+Gelb→Begleiten · 8089 Hellgrün→Läuft gut · ≥90 Dunkelgrün→Fordern.
function triage(pct, active){
if (pct==null || !active) return {lab:'inaktiv', col:'#8a8a8a', bg:'#efeee8'};
if (pct<50) return {lab:'Fördern', col:'#b04a3e', bg:'#f7e2dc'};
if (pct<80) return {lab:'Begleiten', col:'#9a6a12', bg:'#f7edd6'};
if (pct<90) return {lab:'Läuft gut', col:'#3a6b3e', bg:'#dceadd'};
return {lab:'Fordern', col:'#155e2a', bg:'#c8e6cd'};
}
function triageChip(pct, active){ var t=triage(pct,active); return '<span style="display:inline-block;font-size:.62rem;font-weight:700;color:'+t.col+';background:'+t.bg+';padding:1px 7px;border-radius:10px">'+t.lab+'</span>'; }
// Vorschlags-Leistungs-% auf die Testdaten gerechnet (Vorschau).
function _cl(v,a,b){ return Math.max(a,Math.min(b,v)); }
var PERF = {
klima:function(s){ var sp=(s.level>=3)?7000:6000; return Math.round(_cl(35-(s.temp-15.5)*20,0,35)+Math.max(0,25*(1-(s.floodedPct||0)/50))+_cl((s.budget||0)/10,0,20)+_cl((s.population||0)/sp*20,0,20)); },
fluss:function(s){ return _cl(Math.round((s.biodiv+s.economy+s.flood)/3)-(s.settlementsLost||0)*5,0,100); },
kofferdetektiv:function(s){ return s.score!=null?s.score:null; },
tourismusregion:function(s){ return Math.round(0.6*((s.avgStars||0)/5*100)+0.4*(s.env||0)); },
tourismustal:function(s){ if(!s.goalsTotal) return null; return Math.round(0.5*(s.goalsDone/s.goalsTotal*100)+0.3*(s.satisfaction||0)+0.2*(s.nature||0)); },
heli:function(s){ if(s.totalStars!=null) return Math.round(s.totalStars/5*100); if(s.planTotal) return Math.round((s.planCorrect||0)/s.planTotal*100); return null; },
farmer:function(s){ var money=_cl(50+(s.cash||0)/300,0,100),co2=_cl(100-(s.co2_kg||0)/80,0,100),dis=_cl(100-(s.diseases||0)*20,0,100),crop=_cl((s.crops_used||0)/10*100,0,100),country=_cl((s.countries||0)/6*100,0,100); return Math.round(0.25*money+0.20*(s.hit_rate||0)+0.15*co2+0.10*dis+0.10*crop+0.10*country+0.10*70); },
energiemanager:function(s){ var d=(s.day||1)*(s.blockTotal||8); return _cl(Math.round((s.setBlocksBalanced||0)/d*100)-15*(s.blackouts||0),0,100); },
'eu-werkstatt':function(s){ if(!s.stepsTotal) return null; return _cl(Math.round((s.firstTryHits||0)/s.stepsTotal*100)-5*(s.muelleimerCount||0),0,100); },
weltkueche:function(s){ var t=(s.correctClicks||0)+(s.wrongClicks||0); return t?Math.round((s.correctClicks||0)/t*100):null; },
busfahrt:function(s){ return s.orderIndex?Math.round((s.quizFirstTry||0)/s.orderIndex*100):null; },
fluggesellschaft:function(s){ return s.orderIndex?Math.round((s.quizFirstTry||0)/s.orderIndex*100):null; },
sonnensystem:function(s){ return s.taskTotal?Math.round((s.completed||0)/s.taskTotal*100):null; },
logistik:function(s){ return s.balance==null?null:_cl(Math.round(s.balance/16000*100),0,100); },
};
/* ============================================================
4) Testdaten je Sim (3 Schüler:innen: stark · schwach · mittel)
============================================================ */
var NOW = Date.now();
function R(name, emoji, state, o){
o = o || {};
return {
displayName:name, emoji:emoji, module_id:state.__mod, state:state,
started_at_ms: NOW - (o.sinceMin||10)*60000,
last_seen_ms: NOW - (o.seenSec!=null?o.seenSec:4)*1000,
};
}
// missionStartedAt in ms setzen (für die „Seit"-Zeile)
function withMission(state, missMin){ if (missMin!=null) state.missionStartedAt = NOW - missMin*60000; return state; }
var TEST = {
farmer: [
R('Anna','🦊', {__mod:'farmer', year:8, market_de:'Ägypten', cash:14200, hit_rate:78, crops_used:7, countries:5, fields:9, attempts:12, co2_kg:3400, diseases:1}, {sinceMin:24, seenSec:3}),
R('Ben','🐼', {__mod:'farmer', year:4, market_de:'Indien', cash:-800, hit_rate:22, crops_used:3, countries:2, fields:5, attempts:14, co2_kg:5200, diseases:3}, {sinceMin:16, seenSec:6}),
R('Clara','🦉',{__mod:'farmer', year:6, market_de:'Brasilien', cash:5100, hit_rate:54, crops_used:5, countries:4, fields:7, attempts:9, co2_kg:4100, diseases:2}, {sinceMin:19, seenSec:140}),
],
klima: [
R('Anna','🦊', {__mod:'klima', level:2, year:2068, co2:520, temp:16.8, floodedPct:18, budget:1200, population:6400, measures:11, speed:2}, {sinceMin:14}),
R('Ben','🐼', {__mod:'klima', level:1, year:2041, co2:610, temp:17.9, floodedPct:47, budget:-220, population:5200, measures:5, speed:1}, {sinceMin:9, seenSec:5}),
R('Clara','🦉',{__mod:'klima', level:3, year:2085, co2:430, temp:15.9, floodedPct:31, budget:2600, population:8100, measures:16, speed:3}, {sinceMin:22, seenSec:130}),
],
fluss: [
R('Anna','🦊', {__mod:'fluss', health:'good', levelId:'L2', year:64, progressPct:64, score:78, budget:32, biodiv:72, economy:66, flood:81, settlementsLost:0}, {sinceMin:17}),
R('Ben','🐼', {__mod:'fluss', health:'struggling', levelId:'L1', year:20, progressPct:20, score:34, budget:-4, biodiv:41, economy:38, flood:52, settlementsLost:3}, {sinceMin:8, seenSec:7}),
R('Clara','🦉',{__mod:'fluss', health:'ok', levelId:'L2', year:100, progressPct:100, score:88, budget:18, biodiv:80, economy:74, flood:90, settlementsLost:1}, {sinceMin:26, seenSec:150}),
],
energiemanager: [
R('Anna','🦊', {__mod:'energiemanager', health:'good', setTitle:'Frühling', setIcon:'🌱', setIndex:1, setTotal:6, day:3, dayTotal:3, phase:'done', block:7, blockTotal:8, blackouts:0, balanced:8, setBlocksBalanced:21, upperLake:32, lowerLake:28}, {sinceMin:15}),
R('Ben','🐼', {__mod:'energiemanager', health:'struggling', setTitle:'Sommer', setIcon:'🌞', setIndex:2, setTotal:6, day:2, dayTotal:3, phase:'blackout', block:4, blockTotal:8, blackouts:2, balanced:0, setBlocksBalanced:9, upperLake:5, lowerLake:55}, {sinceMin:11, seenSec:6}),
R('Clara','🦉',{__mod:'energiemanager', health:'ok', setTitle:'Herbst', setIcon:'🍂', setIndex:3, setTotal:6, day:1, dayTotal:3, phase:'done', block:5, blockTotal:8, blackouts:1, balanced:6, setBlocksBalanced:6, upperLake:40, lowerLake:20}, {sinceMin:20, seenSec:135}),
],
'eu-werkstatt': [
R('Anna','🦊', {__mod:'eu-werkstatt', health:'good', scenarioTitle:'Käfige abschaffen', scenarioIcon:'🐔', step:6, stepsTotal:6, difficulty:'easy', firstTryHits:6, muelleimerCount:0, reshuffles:0, secondsElapsed:214, achievementsUnlocked:4, achievementsTotal:6}, {sinceMin:6}),
R('Ben','🐼', {__mod:'eu-werkstatt', health:'struggling', scenarioTitle:'Käfige abschaffen', scenarioIcon:'🐔', step:2, stepsTotal:6, difficulty:'easy', firstTryHits:1, muelleimerCount:3, reshuffles:2, secondsElapsed:150, achievementsUnlocked:1, achievementsTotal:6, attemptsThisStep:6}, {sinceMin:5, seenSec:6}),
R('Clara','🦉',{__mod:'eu-werkstatt', health:'ok', scenarioTitle:'Ein Kabel für alle', scenarioIcon:'🔌', step:3, stepsTotal:5, difficulty:'expert', firstTryHits:2, muelleimerCount:1, reshuffles:6, secondsElapsed:330, achievementsUnlocked:2, achievementsTotal:6}, {sinceMin:9, seenSec:150}),
],
weltkueche: [
R('Anna','🦊', {__mod:'weltkueche', dishName:'Tiroler Gröstl', completed:4, completedTotal:6, ingSolved:5, ingTotal:5, correctClicks:38, wrongClicks:4, mistakesInDish:1}, {sinceMin:13}),
R('Ben','🐼', {__mod:'weltkueche', dishName:'Moussaka', completed:1, completedTotal:6, ingSolved:2, ingTotal:6, correctClicks:12, wrongClicks:9, mistakesInDish:7}, {sinceMin:7, seenSec:5}),
R('Clara','🦉',{__mod:'weltkueche', dishName:'Sushi', completed:2, completedTotal:6, ingSolved:6, ingTotal:6, correctClicks:22, wrongClicks:14, mistakesInDish:12}, {sinceMin:10, seenSec:145}),
],
busfahrt: [
R('Anna','🦊', {__mod:'busfahrt', levelId:'L2', orderIndex:8, totalOrders:12, budget:9200, wonOrders:8, lastCityTitle:'Salzburg', lastMargin:420, quizFirstTry:6}, {sinceMin:18}),
R('Ben','🐼', {__mod:'busfahrt', levelId:'L1', orderIndex:3, totalOrders:10, budget:-300, wonOrders:2, lastCityTitle:'Graz', lastMargin:-120, quizFirstTry:1}, {sinceMin:9, seenSec:6}),
R('Clara','🦉',{__mod:'busfahrt', levelId:'L2', orderIndex:5, totalOrders:12, budget:5400, wonOrders:5, lastCityTitle:'Wien', lastMargin:260, quizFirstTry:4}, {sinceMin:14, seenSec:150}),
],
fluggesellschaft: [
R('Anna','🦊', {__mod:'fluggesellschaft', levelId:'L4', orderIndex:6, totalOrders:9, budget:14200, wonOrders:6, co2Kg:8400, lastCityTitle:'Tokio', lastTzDiff:8, quizFirstTry:5}, {sinceMin:21}),
R('Ben','🐼', {__mod:'fluggesellschaft', levelId:'L4', orderIndex:2, totalOrders:9, budget:-500, wonOrders:1, co2Kg:3200, lastCityTitle:'Kairo', lastTzDiff:1, quizFirstTry:1}, {sinceMin:8, seenSec:5}),
R('Clara','🦉',{__mod:'fluggesellschaft', levelId:'L4', orderIndex:4, totalOrders:9, budget:7600, wonOrders:4, co2Kg:5600, lastCityTitle:'New York', lastTzDiff:-6, quizFirstTry:3}, {sinceMin:12, seenSec:150}),
],
heli: [
R('Anna','🦊', withMission({__mod:'heli', phase:'results', missionId:'M19', missionTitle:'Seerettung', planIndex:6, planTotal:6, planCorrect:6, planAttempts:1, startScore:5, landingScore:4, totalStars:5, failed:false}, 5), {sinceMin:12}),
R('Ben','🐼', withMission({__mod:'heli', phase:'flight', missionId:'M7', missionTitle:'Kufstein', planIndex:3, planTotal:5, planCorrect:2, planAttempts:11, startScore:3, landingScore:null, totalStars:null, failed:false}, 3), {sinceMin:8, seenSec:6}),
R('Clara','🦉',withMission({__mod:'heli', phase:'mission', missionId:'M5', missionTitle:'Bregenz', planIndex:0, planTotal:5, planCorrect:0, planAttempts:0, startScore:null, landingScore:null, totalStars:null, failed:false, lastResult:true, lastFailed:false}, 1), {sinceMin:14, seenSec:150}),
],
kofferdetektiv: [
R('Anna','🦊', {__mod:'kofferdetektiv', health:'good', phase:'suche', progressPct:60, score:82, cases_solved:6, cases_total:10, case_iso2:'IT', hints_revealed:1, wrong_guesses_total:2, time_left_s:180, airport:'Rom-Fiumicino'}, {sinceMin:16}),
R('Ben','🐼', {__mod:'kofferdetektiv', health:'ok', phase:'suche', progressPct:30, score:55, cases_solved:3, cases_total:10, case_iso2:'BR', hints_revealed:4, wrong_guesses_total:7, time_left_s:45, airport:'São Paulo-Guarulhos'}, {sinceMin:11, seenSec:6}),
R('Clara','🦉',{__mod:'kofferdetektiv', health:'struggling', phase:'briefing', progressPct:10, score:28, cases_solved:1, cases_total:10, case_iso2:'JP', hints_revealed:6, wrong_guesses_total:9, time_left_s:20, airport:'Tokio-Narita'}, {sinceMin:6, seenSec:150}),
],
sonnensystem: [
R('Anna','🦊', {__mod:'sonnensystem', health:'good', screen:'play', taskTitle:'Mondphasen', taskIndex:8, taskTotal:20, completed:8, progressPct:40, simDate:'14.03.', achievementsUnlocked:3, achievementsTotal:8}, {sinceMin:13}),
R('Ben','🐼', {__mod:'sonnensystem', health:'ok', screen:'explore', taskTitle:'', taskIndex:0, taskTotal:20, completed:14, progressPct:70, simDate:'22.09.', achievementsUnlocked:5, achievementsTotal:8}, {sinceMin:19, seenSec:5}),
R('Clara','🦉',{__mod:'sonnensystem', health:'struggling', screen:'play', taskTitle:'Sonnenfinsternis', taskIndex:3, taskTotal:20, completed:2, progressPct:10, simDate:'02.07.', achievementsUnlocked:1, achievementsTotal:8}, {sinceMin:7, seenSec:150}),
],
tourismustal: [
R('Anna','🦊', {__mod:'tourismustal', scene:'boom', season:'Sommer', year:2004, money:8200, guestsToday:180, satisfaction:74, nature:52, goalsDone:2, goalsTotal:3, buildings:6, bottleneck:''}, {sinceMin:20}),
R('Ben','🐼', {__mod:'tourismustal', scene:'klimawandel', season:'Winter', year:2037, money:-600, guestsToday:60, satisfaction:38, nature:41, goalsDone:1, goalsTotal:3, buildings:9, bottleneck:'Schnee'}, {sinceMin:10, seenSec:6}),
R('Clara','🦉',{__mod:'tourismustal', scene:'pionierzeit', season:'Sommer', year:1978, money:4300, guestsToday:120, satisfaction:63, nature:78, goalsDone:1, goalsTotal:3, buildings:3, bottleneck:''}, {sinceMin:15, seenSec:150}),
],
tourismusregion: [
R('Anna','🦊', {__mod:'tourismusregion', mapId:'talschleife', wave:8, waveTotal:8, budget:22000, revenue:180000, avgStars:3.8, env:72, visitors:540, goalsDone:3, goalsTotal:3}, {sinceMin:23}),
R('Ben','🐼', {__mod:'tourismusregion', mapId:'seerunde', wave:3, waveTotal:8, waveStatus:'pause', budget:-400, revenue:42000, avgStars:2.1, env:34, visitors:210, goalsDone:1, goalsTotal:3}, {sinceMin:9, seenSec:6}),
R('Clara','🦉',{__mod:'tourismusregion', mapId:'serpentinen', wave:5, waveTotal:8, budget:8600, revenue:96000, avgStars:2.9, env:58, visitors:330, goalsDone:2, goalsTotal:3}, {sinceMin:16, seenSec:150}),
],
logistik: [
R('Anna','🦊', {__mod:'logistik', levelId:'L1', balance:9200, ordersDelivered:8, totalOrders:12, ordersInFlight:2, ordersOpen:2, ordersLate:0, progressPct:66}, {sinceMin:18}),
R('Ben','🐼', {__mod:'logistik', levelId:'L2', balance:-300, ordersDelivered:3, totalOrders:14, ordersInFlight:3, ordersOpen:8, ordersLate:2, progressPct:21}, {sinceMin:9, seenSec:6}),
R('Clara','🦉',{__mod:'logistik', levelId:'L1', balance:5400, ordersDelivered:6, totalOrders:12, ordersInFlight:1, ordersOpen:5, ordersLate:1, progressPct:50}, {sinceMin:13, seenSec:150}),
],
};
/* ============================================================
5) Rendern — Tabelle wie renderLive(), plus Grün/Grau + Keys
============================================================ */
var _LIVE_PAUSE_SEC = 90;
function _liveIsActive(ls){ return ls.last_seen_ms && (NOW - ls.last_seen_ms) <= _LIVE_PAUSE_SEC*1000; }
function renderScoring(id){
var sc = SCORING[id];
if (!sc) return '';
var h = '<div class="scorebox"><h4>🎯 So entsteht die Bewertung</h4>';
h += '<div class="srow"><span class="lab">Sterne</span><span>'+sc.stars+'</span></div>';
h += '<div class="srow"><span class="lab">Prozent</span><span>'+sc.pct+'</span></div>';
if (PROPOSED_PCT[id]) h += '<div class="srow" style="background:#eef7f0;border-radius:6px;padding:.25rem .4rem;margin-top:.35rem"><span class="lab" style="color:#3a6b3e">Vorschlag %</span><span style="color:#2f5e39">'+PROPOSED_PCT[id]+'</span></div>';
(sc.flags||[]).forEach(function(f){ h += '<div class="flag">⚠ '+f+'</div>'; });
h += '</div>';
return h;
}
function renderSim(meta){
var fields = LIVE_PRIMARY_FIELDS[meta.id] || [];
var rows = TEST[meta.id] || [];
var greenSet = {}; (GREEN[meta.id]||[]).forEach(function(k){ greenSet[k]=true; });
var greyKeys = fields.filter(function(f){ return !greenSet[f.k]; }).map(function(f){ return f.k; });
var greenCount = fields.length - greyKeys.length;
// Spalten-Maxima für relative Mini-Balken
var colMax = {};
fields.forEach(function(f){ var mx=0; rows.forEach(function(r){ var val=(r.state||{})[f.k]; if (typeof val==='number' && !isNaN(val)) mx=Math.max(mx,Math.abs(val)); }); colMax[f.k]=mx; });
var h = '<div class="sim" id="sim-'+meta.id+'">';
h += '<div class="sim-head"><span class="emoji">'+meta.icon+'</span><h3>'+esc(meta.title)+'</h3>'
+ '<span class="sid">'+meta.id+'</span>'
+ '<span class="count">'+greenCount+' grün · '+greyKeys.length+' nur live</span></div>';
h += '<div class="tbl-scroll"><table class="live"><thead><tr>';
h += '<th style="text-align:left">Schüler*in<span class="key">displayName</span></th>';
h += '<th style="text-align:left;color:#8a8a8a">Seit<span class="key">missionStartedAt / started_at</span></th>';
h += '<th style="text-align:left;background:#e7f4ea;border-bottom-color:#bcd9c2">🎯 Leistung<span class="key">leistung_pct (Vorschlag)</span><span class="tag g">EIN Wert</span></th>';
fields.forEach(function(f){
var align=f.align||'right';
var isG=!!greenSet[f.k];
h += '<th class="'+(isG?'green':'grey')+'" style="text-align:'+align+'">'
+ esc(f.label)+(f.unit?' <span style="color:#8a8a8a;font-weight:400;font-size:.66rem">'+f.unit+'</span>':'')
+ '<span class="key">'+esc(f.k)+'</span>'
+ '<span class="tag '+(isG?'g':'l')+'">'+(isG?'nach Abschluss':'nur live')+'</span>'
+ '</th>';
});
h += '<th style="width:1.2em"></th></tr></thead><tbody>';
rows.forEach(function(ls){
var s = ls.state||{};
var avatarHtml = '<span style="font-size:1.1rem">'+(ls.emoji||'🧑‍🎓')+'</span>';
var sinceSimBase = ls.started_at_ms||0;
var sinceMissionBase = (s.missionStartedAt)||(s.levelStartedAt)||(s.scenarioStartedAt)||0;
var sinceSim = sinceSimBase ? Math.max(0,Math.round((NOW-sinceSimBase)/1000)) : 0;
var sinceMission = sinceMissionBase ? Math.max(0,Math.round((NOW-sinceMissionBase)/1000)) : 0;
var simStr = sinceSimBase ? _fmtSince(sinceSim) : '—';
var missionStr = sinceMissionBase ? _fmtSince(sinceMission) : '—';
var lastSeen = ls.last_seen_ms ? Math.max(0,Math.round((NOW-ls.last_seen_ms)/1000)) : 0;
var paused = !_liveIsActive(ls) || !!(s.__idlePaused||s.__paused);
var dotColor = lastSeen<8?'#5a8a5e':lastSeen<20?'#e8833a':'#c07a6b';
var help = !paused && _liveNeedsHelp(ls.module_id, s);
var restBg = help ? '#fdf3ee' : (paused ? '#f6f5f2' : '');
var accent = help ? 'inset 3px 0 0 #c0574a' : (paused ? 'inset 3px 0 0 #cfcabb' : 'none');
h += '<tr style="background:'+restBg+';'+(paused?'color:#9a9a9a;':'')+'box-shadow:'+accent+'">';
h += '<td style="white-space:nowrap"><span style="display:inline-flex;align-items:center;gap:.4rem">'+(help?'<span title="stockt · braucht evtl. Hilfe">🆘</span>':(paused?'<span title="pausiert">⏸</span>':''))+avatarHtml+'<strong>'+esc(ls.displayName)+'</strong></span></td>';
h += '<td style="white-space:nowrap;line-height:1.15"><div style="font-size:.78rem;color:#3a3a3a;font-weight:600">'+missionStr+'</div><div style="font-size:.62rem;color:#8a8a8a">'+simStr+' ges.</div></td>';
var perfPct = (PERF[meta.id]) ? PERF[meta.id](s) : null;
h += '<td style="background:#f2faf4;vertical-align:top">'+pctBar(perfPct)+'<div style="margin-top:3px">'+triageChip(perfPct, perfPct!=null)+'</div></td>';
fields.forEach(function(f){
var align=f.align||'right';
var isG=!!greenSet[f.k];
var cell=_liveFmt(s[f.k], f.unit, f, ls);
if (f.bar){
var raw=s[f.k];
if (typeof raw==='number' && !isNaN(raw)){
var pct=0, bcol;
if (f.bar.kind==='abs'){ pct=(raw/(f.bar.max||100))*100; bcol=_liveBarColor(pct,f.bar.good); }
else { var mx=colMax[f.k]||0; pct=mx>0?(Math.abs(raw)/mx)*100:0; bcol=(f.bar.good==='signed')?(raw<0?'#c0392b':'#5a8a5e'):_liveBarColor(pct,f.bar.good); }
cell='<div style="line-height:1.1">'+cell+'</div>'+_liveMiniBar(pct,bcol);
}
} else if (typeof s[f.k]==='number' && !isNaN(s[f.k]) && (colMax[f.k]||0)>0 && cell.indexOf('border-radius:99px')<0){
var apct=(Math.abs(s[f.k])/colMax[f.k])*100;
cell='<div style="line-height:1.1">'+cell+'</div>'+_liveMiniBar(apct,'#9aa7b0');
}
h += '<td class="'+(isG?'green':'')+'" style="text-align:'+align+'">'+cell+'</td>';
});
h += '<td style="text-align:center">'+(paused
? '<span style="font-size:.6rem;color:#9a9a9a;white-space:nowrap">⏸ '+_fmtSince(lastSeen)+'</span>'
: '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+dotColor+'"></span>')+'</td>';
h += '</tr>';
});
h += '</tbody></table></div>';
// Bewertung: wie Sterne + Prozent entstehen
h += renderScoring(meta.id);
// Nach-Abschluss-Hinweis
if (greyKeys.length){
h += '<div class="note"><b>Nach Abschluss NICHT relevant (nur live):</b><ul>';
greyKeys.forEach(function(k){
var lbl=(fields.filter(function(f){return f.k===k;})[0]||{}).label||k;
h += '<li><code>'+esc(k)+'</code> · '+esc(lbl)+' — '+esc(GREY_REASON[k]||'momentaner Zustand, ändert sich laufend')+'</li>';
});
h += '</ul></div>';
} else {
h += '<div class="allgreen">✓ Alle Spalten dieser Sim bleiben auch nach Abschluss relevant.</div>';
}
h += '</div>';
return h;
}
// Vorschau-Karte: neues Bewertungssystem (Skala · %+Balken · Triage)
function renderPreviewCard(){
// EINE Skala. Jede Zeile: Farbe · Bereich · Label · Beispiel-Balken.
var scale=[
{c:'#1f7a3d', r:'≥ 90 %', l:'sehr gut', v:90},
{c:'#7cb342', r:'8089 %', l:'gut', v:80},
{c:'#f9c33b', r:'6079 %', l:'solide', v:60},
{c:'#ef8b2c', r:'5059 %', l:'wackelig', v:50},
{c:'#d8453a', r:'< 50 %', l:'kritisch', v:40},
];
var h='<div class="gbox" style="border-left-color:#5a8a5e">';
h+='<h2>🎯 Bewertung — EINE Skala</h2>';
h+='<p><b>Standard überall im Programm:</b> ein Leistungs-% (0100) in der Ampelfarbe + schmaler Balken. Sterne fallen weg. Es gibt nur <b>diese eine</b> Skala mit 5 Stufen:</p>';
h+='<div style="display:grid;grid-template-columns:24px auto 1fr 48px;gap:.4rem .9rem;align-items:center;max-width:460px;margin:.5rem 0 1rem;font-size:.82rem">';
scale.forEach(function(b){
h+='<span style="width:20px;height:20px;border-radius:5px;background:'+b.c+'"></span>';
h+='<b style="font-variant-numeric:tabular-nums">'+b.r+'</b>';
h+='<span style="color:#5a5a5a">'+b.l+'</span>';
h+='<span>'+pctBar(b.v)+'</span>';
});
h+='</div>';
h+='<p><b>Triage</b> — dieselbe Skala, zu Handlungs-Feldern zusammengefasst (Grenzen = Farbgrenzen 50 / 80 / 90):</p>';
h+='<div style="display:flex;gap:1.3rem;flex-wrap:wrap;align-items:center;margin:.3rem 0 .2rem;font-size:.78rem">'
+'<span>'+triageChip(30,true)+' &nbsp;<b>&lt; 50 %</b> <span style="color:#8a8a8a">(rot)</span></span>'
+'<span>'+triageChip(65,true)+' &nbsp;<b>5079 %</b> <span style="color:#8a8a8a">(orange + gelb)</span></span>'
+'<span>'+triageChip(85,true)+' &nbsp;<b>8089 %</b> <span style="color:#8a8a8a">(hellgrün)</span></span>'
+'<span>'+triageChip(95,true)+' &nbsp;<b>≥ 90 %</b> <span style="color:#8a8a8a">(dunkelgrün)</span></span>'
+'<span>'+triageChip(null,false)+' &nbsp;<span style="color:#8a8a8a">nicht aktiv</span></span>'
+'</div>';
h+='<p style="font-size:.72rem;color:#8a8a8a;margin-top:.8rem">Unten hat jede Tabelle die Spalte <b>🎯 Leistung</b> — die Vorschlags-Formel je Sim, auf die Testdaten gerechnet. Bei Busfahrt · Fluggesellschaft · Sonnensystem ist der Wert eine Näherung (Topo-/Erstversuch-Rohwerte fehlen in den Testdaten).</p>';
h+='</div>';
return h;
}
// TOC + Inhalt aufbauen
var toc = SIM_META.map(function(m){ return '<a href="#sim-'+m.id+'">'+m.icon+' '+esc(m.title)+'</a>'; }).join('');
document.getElementById('preview').innerHTML = renderPreviewCard();
document.getElementById('toc').innerHTML = toc;
document.getElementById('content').innerHTML = SIM_META.map(renderSim).join('');
</script>
</body>
</html>