Lehrer-Drilldown: grafische Kennzahlen, Avatar-Bild, Sterne & Abzeichen

- sim-metrics.js: kuratiertes Feld-Schema pro Sim (Sterne/Prozentbalken/
  Verhältnis/Geld/Temperatur), technische Felder ausgeblendet, sauber benannt
- Drilldown zeigt Avatar als Bild (avatar:<slug>), Erfolg als Sterne,
  Parameter als grafische Kacheln, Abzeichen-Sammlung pro gespielter Sim
- live.php student_runs liefert player_progress fuer die Abzeichen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 15:35:29 +02:00
parent 2db99c37ee
commit c83da66325
3 changed files with 209 additions and 9 deletions
+44 -9
View File
@@ -413,6 +413,7 @@ schueler01,Max M.,,,,,</pre>
</div>
<script src="assets/js/badges.js"></script>
<script src="assets/js/sim-metrics.js"></script>
<script>
var BASE = (window.__GGS__?.baseUrl) || '';
if (!BASE) { var p = location.pathname, i = p.indexOf('/geograsim'); if (i !== -1) { var rest = p.substring(i + '/geograsim'.length); BASE = rest.indexOf('/App/') === 0 ? p.substring(0, i) + '/geograsim/App' : p.substring(0, i) + '/geograsim'; } }
@@ -2137,8 +2138,15 @@ var IV_CSS = ''
+ '.iv-live{background:color-mix(in srgb,var(--ivg) 12%,transparent);border:1px solid color-mix(in srgb,var(--ivg) 30%,var(--ivline));border-radius:10px;padding:10px 14px;margin-bottom:14px;font-size:.82rem}'
+ '.iv-run{border:1px solid var(--ivline);border-radius:10px;padding:9px 12px;margin-bottom:8px}'
+ '.iv-run .rh{display:flex;align-items:center;gap:8px;font-size:.84rem;font-weight:700;margin-bottom:6px}.iv-run .rh .sc{font-weight:800}.iv-run .rh .d{margin-left:auto;font-size:.68rem;color:var(--ivmut);font-weight:400}'
+ '.iv-params{display:flex;flex-wrap:wrap;gap:5px}'
+ '.iv-pchip{display:inline-flex;gap:5px;align-items:baseline;background:var(--ivsoft);border-radius:6px;padding:2px 7px;font-size:.7rem}.iv-pchip .pk{color:var(--ivmut)}.iv-pchip .pv{font-weight:700}'
+ '.ivm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(128px,1fr));gap:7px}'
+ '.ivm{background:var(--ivsoft);border-radius:8px;padding:6px 9px;min-width:0}'
+ '.ivm-l{display:block;font-size:.58rem;color:var(--ivmut);text-transform:uppercase;letter-spacing:.03em;margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}'
+ '.ivm-v{font-size:.92rem;font-weight:750}.ivm-u{font-size:.66rem;font-weight:600;color:var(--ivmut)}.ivm-txt{font-size:.8rem;font-weight:700}'
+ '.ivm-stars{color:#e0a838;font-size:.95rem;letter-spacing:1px}.ivm-stars.sm{font-size:.8rem}.ivm-star-off{color:var(--ivtrack)}'
+ '.ivm-bar{display:block;height:7px;border-radius:4px;background:var(--ivtrack);overflow:hidden;margin-top:3px}.ivm-bar>i{display:block;height:100%;border-radius:4px;background:currentColor}'
+ '.ivm-g{color:var(--ivg)}.ivm-li{color:var(--ivli)}.ivm-y{color:var(--ivy)}.ivm-o{color:var(--ivo)}.ivm-r{color:var(--ivr)}'
+ '.iv-badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}.iv-badge{width:38px;height:38px}.iv-badge img{width:100%;height:100%;object-fit:contain}.iv-badge.locked img{filter:grayscale(.9);opacity:.4}'
+ '.iv-mav{width:38px;height:38px;border-radius:50%;object-fit:cover}'
+ '@media (prefers-reduced-motion:reduce){.iv-dotA{animation:none!important}}';
function ivInjectCss(){ if(!document.getElementById('iv-css')){ var s=document.createElement('style'); s.id='iv-css'; s.textContent=IV_CSS; document.head.appendChild(s); } }
@@ -2302,6 +2310,15 @@ function ivParams(p){
});
return chips.length ? chips.join('') : '<span style="font-size:.7rem;color:var(--ivmut)">keine Parameter</span>';
}
function ivAvatar(emoji){
if (typeof emoji==='string' && emoji.indexOf('avatar:')===0){
var slug = emoji.slice(7).toLowerCase();
return '<img class="iv-mav" src="assets/img/avatars/thumbs/avatar-'+encodeURIComponent(slug)+'.webp" alt="" '
+ 'onerror="this.onerror=null;this.src=\'assets/img/avatars/thumbs/avatar-fuchs-explorer.webp\'">';
}
return '<span class="em" style="font-size:1.6rem">'+(emoji||'🧑‍🎓')+'</span>';
}
function ivStars(n){ return (typeof ggsStarsHtml==='function') ? ggsStarsHtml(n) : ('★'.repeat(Math.max(0,Math.min(5,Math.round(n)))) ); }
async function ivOpenStudent(sid){
ivInjectCss();
var d = await api('live?student_runs='+sid);
@@ -2310,20 +2327,38 @@ async function ivOpenStudent(sid){
ov.onclick = function(e){ if (e.target===ov) ov.remove(); };
document.addEventListener('keydown', function esc0(e){ if(e.key==='Escape'){ var o=document.getElementById('iv-ov'); if(o)o.remove(); document.removeEventListener('keydown', esc0);} });
if (!d || d.error){ ov.innerHTML='<div class="iv-modal">Konnte nicht laden.</div>'; document.body.appendChild(ov); return; }
var h='<div class="iv-modal"><div class="iv-mh"><span class="em">'+(d.emoji||'🧑‍🎓')+'</span><h3>'+esc(d.displayName)+'</h3>'
var sm = (typeof renderSimMetrics==='function') ? renderSimMetrics : function(){return '';};
var erfStars = (typeof ggsErfolgToStars==='function' && d.recentErfolg!=null) ? ggsErfolgToStars(d.recentErfolg) : null;
var h='<div class="iv-modal"><div class="iv-mh">'+ivAvatar(d.emoji)+'<h3>'+esc(d.displayName)+'</h3>'
+ '<button class="x" onclick="document.getElementById(\'iv-ov\').remove()">×</button></div>';
h+='<div class="iv-sum"><span>Erfolg (letzte Läufe)<b class="num">'+(d.recentErfolg!=null?d.recentErfolg:'')+'</b></span>'
+ '<span>Zuletzt aktiv<b>'+(d.daysSince==null?'nie':(d.daysSince<=0?'heute':(d.daysSince==1?'gestern':'vor '+d.daysSince+' T')))+'</b></span></div>';
h+='<div class="iv-sum">'
+ '<span>Erfolg (letzte Läufe) '+(erfStars!=null?'<b>'+ivStars(erfStars)+'</b> <span class="ivm-u">'+d.recentErfolg+'/100</span>':'<b></b>')+'</span>'
+ '<span>Zuletzt aktiv <b>'+(d.daysSince==null?'nie':(d.daysSince<=0?'heute':(d.daysSince==1?'gestern':'vor '+d.daysSince+' T')))+'</b></span></div>';
if (d.live){ var lm=ivModMeta(d.live.moduleId);
h+='<div class="iv-live"><b>Arbeitet gerade:</b> '+lm.icon+' '+esc(lm.title)+'<div class="iv-params" style="margin-top:7px">'+ivParams(d.live.state)+'</div></div>'; }
h+='<div style="font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ivmut);margin-bottom:7px">Letzte Durchläufe · Simulationsparameter</div>';
h+='<div class="iv-live"><b>Arbeitet gerade:</b> '+lm.icon+' '+esc(lm.title)+'<div style="margin-top:8px">'+sm(d.live.moduleId, d.live.state)+'</div></div>'; }
// Abzeichen (aus player_progress, nur gespielte Sims)
if (typeof GGS_SIMS!=='undefined' && d.progress && d.progress.length){
var pmap={}; d.progress.forEach(function(p){ pmap[p.simId]=p; });
var anyBadge=false, bh='';
GGS_SIMS.forEach(function(sim){
var p=pmap[sim.id]; if(!p) return; anyBadge=true;
bh+='<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"><span style="font-size:.76rem;font-weight:700;width:118px;flex:none">'+sim.icon+' '+esc(sim.title)+'</span><div class="iv-badges">';
ggsSimBadges(sim.id).forEach(function(b){ var e=b.cond(p);
bh+='<span class="iv-badge'+(e?'':' locked')+'" title="'+esc(e?b.name:'Ziel: '+b.desc)+'"><img src="'+b.icon+'" alt="'+esc(b.name)+'"></span>'; });
bh+='</div></div>';
});
if(anyBadge) h+='<div style="font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ivmut);margin:2px 0 7px">Abzeichen</div>'+bh;
}
h+='<div style="font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ivmut);margin:8px 0 7px">Letzte Durchläufe</div>';
if (!d.runs || !d.runs.length){ h+='<p style="font-size:.8rem;color:var(--ivmut)">Noch keine Durchläufe.</p>'; }
(d.runs||[]).forEach(function(r){
var m=ivModMeta(r.moduleId);
var dt = r.completedAt ? new Date(r.completedAt.replace(' ','T')).toLocaleDateString('de-AT',{day:'2-digit',month:'2-digit',year:'2-digit'}) : '';
h+='<div class="iv-run"><div class="rh">'+m.icon+' '+esc(m.title)+'<span class="sc">'+(r.score!=null?Math.round(r.score):'')+'</span>'
var rst=(typeof ggsErfolgToStars==='function'&&r.score!=null)?ggsErfolgToStars(r.score):null;
h+='<div class="iv-run"><div class="rh">'+m.icon+' '+esc(m.title)
+ '<span class="sc" style="color:#e0a838">'+(rst!=null?ivStars(rst):(r.score!=null?Math.round(r.score):''))+'</span>'
+ '<span class="d">'+dt+' · '+Math.round((r.durationSec||0)/60)+' min</span></div>'
+ '<div class="iv-params">'+ivParams(r.params)+'</div></div>';
+ sm(r.moduleId, r.params)+'</div>';
});
h+='</div>';
ov.innerHTML=h; document.body.appendChild(ov);