c79ef691ad
- Vorschau-Karte: %+Balken-Standardanzeige, 5-stufige Farbskala, Triage-Felder (Foerdern/Begleiten/Laeuft gut) aus %xFleiss mit Grenzen = Farbgrenzen (50/65), damit Farbe<->Feld immer stimmen. - Neue Spalte 'Leistung' je Sim: Vorschlags-Formel auf die Testdaten gerechnet (PERF), als pctBar + Triage-Chip. Busfahrt/Flug/Sonnensystem Naeherung (Rohwerte fehlen in Testdaten). - Bewertungs-Block je Sim um 'Vorschlag %' (PROPOSED_PCT) erweitert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
810 lines
69 KiB
HTML
810 lines
69 KiB
HTML
<!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 / 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 & 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>0–100-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>0–3 Sterne</b> statt 0–5. Busfahrt/Fluggesellschaft senden als <code>score</code> rohe Punkte (nicht 0–100 → Zeilen-% verzerrt). Weltküche wertet <b>pro Gericht</b> statt pro Durchgang. <b>Logistik überträgt weder Sterne noch Score</b>. Ein echter 0–100-Live-Score existiert nur bei Klima, Fluss, Heli, Kofferdetektiv, Tourismusregion (abgeleitet Tourismustal).</p>
|
||
<div class="rec"><b>Vorschlag (ein System):</b> Jede App liefert genau <b>eine Funktion → Leistungs-% (0–100)</b> als Wahrheit (grün→rot-Balken). Sterne werden daraus mit <b>denselben globalen Schwellen für alle</b> abgeleitet (z. B. ≥90→5★ · ≥75→4 · ≥55→3 · ≥35→2 · sonst 1). Dann bedeuten Sterne überall dasselbe und % ↔ Sterne sind konsistent.</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 & 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>0–5</b> aus <code>totalScore</code> (0–100, 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 0–100-Live-Score → Zeilen-% = <code>Sterne/5×100</code> (im Ergebnis liegt zusätzlich <code>data.score</code>).',
|
||
},
|
||
klima: {
|
||
stars:'<b>0–5</b>: Basis <code>2★</code> bei Sieg, dann <code>+1</code> je Temp<17° · Temp<16,3° · Budget>200 · (Flut<20% & Bev.>90% Start). Deckel: Temp≥17°→max 3★, ≥18,5°→max 1★.',
|
||
pct:'Eigener 0–100-Score = <code>TempScore(0–35)+FlutScore(0–25)+BudgetScore(0–20)+BevScore(0–20)</code>. Live-Score identisch → Zeilen-% = dieser Score.',
|
||
},
|
||
fluss: {
|
||
stars:'<b>0–5</b> aus Score: <code>≥85→5 · ≥70→4 · ≥55→3 · ≥35→2 · sonst 1</code>.',
|
||
pct:'Score 0–100 = <code>Ø(Biodiv, Wirtschaft, Hochwasserschutz) − 5×Siedlungsverluste</code>. Live-Score vorhanden → Zeilen-% = Score.',
|
||
},
|
||
energiemanager: {
|
||
stars:'<b>0–5</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 0–100-Live-Score → Zeilen-% = <code>Sterne/5×100</code>.',
|
||
},
|
||
'eu-werkstatt': {
|
||
stars:'<b>0–5</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 0–100-Live-Score → Zeilen-% = <code>Sterne/5×100</code>.',
|
||
},
|
||
weltkueche: {
|
||
stars:'<b>0–5 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 0–100) → 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 0–3</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 0–100).',
|
||
flags:['0–3 statt 0–5 Sterne.','score ist keine 0–100-Zahl → Zeilen-% verzerrt.'],
|
||
},
|
||
fluggesellschaft: {
|
||
stars:'<b>nur 0–3</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 0–100).',
|
||
flags:['0–3 statt 0–5 Sterne.','score ist keine 0–100-Zahl → Zeilen-% verzerrt.'],
|
||
},
|
||
heli: {
|
||
stars:'<b>0–5</b> = <code>round((PlanSterne + Start + Landung) / 3)</code>. PlanSterne aus Timing (Ø≥1,6→5 …) bzw. Klickgenauigkeit; Chefpilot-Übernahme → 0.',
|
||
pct:'Live-Score 0–100 = <code>Sterne/5×100</code> (submit <code>score = Sterne×20</code>) → Zeilen-% = das.',
|
||
},
|
||
kofferdetektiv: {
|
||
stars:'<b>nur 0–3</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:['0–3 statt 0–5 Sterne.'],
|
||
},
|
||
sonnensystem: {
|
||
stars:'<b>1–5</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 0–100-Live-Score (nur <code>levelWon</code>+Fortschritt; <code>scoreQuote</code> im data) → Zeilen-% = <code>Sterne/5×100</code>.',
|
||
},
|
||
tourismustal: {
|
||
stars:'<b>0–5</b> = <code>min(5, erreichte Ziele + (Sieg?1:0))</code>.',
|
||
pct:'Score 0–100 = <code>erreichte Ziele / gesamt × 100</code>; kein Live-0–100 → Zeilen-% = data.score bzw. Sterne/5×100.',
|
||
},
|
||
tourismusregion: {
|
||
stars:'<b>0–5</b> = <code>round(Ø Besucher-Bewertung)</code> (avgStars 0–5).',
|
||
pct:'Score 0–100 = gewichtet: <code>Bewertung 35% · Wertschöpfung 25% · Nachhaltigkeit 20% · Lenkung 10% · Finanzen 10%</code>. Live <code>avgStars</code> → Zeilen-% = Score.',
|
||
},
|
||
logistik: {
|
||
stars:'<b>0–5</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 & 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-% (0–100) je Sim,
|
||
5-stufige Farbskala, %+Balken-Komponente, Triage (%×Fleiß).
|
||
============================================================ */
|
||
var PROPOSED_PCT = {
|
||
farmer:'gewichteter 0–100-Score (wie heute intern berechnet) — künftig auch LIVE senden.',
|
||
klima:'vorhandener 0–100-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 (0–100), nicht gerundete Sterne.',
|
||
kofferdetektiv:'vorhandener score100 (0–100) übernehmen.',
|
||
sonnensystem:'Erstversuch-Quote ×100.',
|
||
tourismustal:'Ziele/gesamt ×100, gewichtet mit Zufriedenheit/Natur.',
|
||
tourismusregion:'vorhandener gewichteter 0–100-Score übernehmen.',
|
||
logistik:'Kontostand→% (0 €→0, Zielbudget→100) — UND ans Backend senden.',
|
||
};
|
||
|
||
// 5-stufige Farbskala (= _tagTraffic).
|
||
function pctColor(v){ v=Math.max(0,Math.min(100,v||0)); return v>=80?'#1f7a3d':v>=65?'#7cb342':v>=50?'#f9c33b':v>=35?'#ef8b2c':'#d8453a'; }
|
||
// Standard-Anzeige überall: Prozent in Ampelfarbe + dünner Balken darunter.
|
||
function pctBar(pct){
|
||
if (pct==null) return '<span style="color:#9a9a9a">—</span>';
|
||
var c=pctColor(pct);
|
||
return '<div style="min-width:72px">'
|
||
+ '<div style="font-weight:800;color:'+c+';font-size:.86rem;font-variant-numeric:tabular-nums">'+pct+' %</div>'
|
||
+ '<div style="margin-top:2px;height:6px;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.
|
||
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<65) return {lab:'Begleiten', col:'#9a6a12', bg:'#f7edd6'};
|
||
return {lab:'Läuft gut', col:'#3a6b3e', bg:'#dceadd'};
|
||
}
|
||
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(){
|
||
var samples=[92,71,55,40,22];
|
||
var h='<div class="gbox" style="border-left-color:#5a8a5e">';
|
||
h+='<h2>🎯 Neues Bewertungssystem — Vorschau zum Abnehmen</h2>';
|
||
h+='<p><b>Standard überall:</b> ein Leistungs-% (0–100) in der Ampelfarbe + dünner Balken darunter — Sterne fallen weg. Beispiele:</p>';
|
||
h+='<div style="display:flex;gap:1.4rem;flex-wrap:wrap;margin:.4rem 0 .9rem">'+samples.map(function(v){return pctBar(v);}).join('')+'</div>';
|
||
h+='<p><b>5-stufige Farbskala</b> (= bestehende Tag-Skala):</p>';
|
||
h+='<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin:.3rem 0 .9rem;font-size:.72rem">'
|
||
+[['≥ 80','sehr gut',92],['65–79','gut',71],['50–64','solide',55],['35–49','wackelig',40],['< 35','kritisch',22]]
|
||
.map(function(b){var c=pctColor(b[2]);return '<span style="display:inline-flex;align-items:center;gap:.35rem;background:#fff;border:1px solid rgba(0,0,0,.08);border-radius:20px;padding:.2rem .6rem"><span style="width:14px;height:14px;border-radius:4px;background:'+c+'"></span><b>'+b[0]+' %</b> · '+b[1]+'</span>';}).join('')+'</div>';
|
||
h+='<p><b>Triage-Felder aus %×Fleiß</b> — die Grenzen sind die Farbgrenzen, damit Balkenfarbe und Feld immer übereinstimmen:</p>';
|
||
h+='<div style="display:flex;gap:1.2rem;flex-wrap:wrap;margin:.3rem 0 .6rem">'
|
||
+[22,55,80].map(function(v){return '<div style="text-align:center">'+pctBar(v)+'<div style="margin-top:3px">'+triageChip(v,true)+'</div></div>';}).join('')
|
||
+'<div style="text-align:center">'+pctBar(null)+'<div style="margin-top:3px">'+triageChip(null,false)+'</div></div>'+'</div>';
|
||
h+='<div class="rec"><b>< 50 % → Fördern</b> · <b>50–64 % → Begleiten</b> · <b>≥ 65 % → Läuft gut</b> — jeweils nur, wenn aktiv gearbeitet wurde; sonst „inaktiv".</div>';
|
||
h+='<p style="font-size:.72rem;color:#8a8a8a;margin-top:.6rem">Unten hat jede Tabelle die neue 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>
|