Balancing: sonnensystem (Erst-Treffer-Bewertung, Loesung nicht aufdecken, Level-Badge) + energiemanager (echter Blackout an Netzfrequenz, Schwierigkeitsrampe)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+109
-39
@@ -572,6 +572,17 @@
|
||||
.obs-option.correct { background: #dcfce7; border-color: #16a34a; }
|
||||
.obs-option.wrong { background: #fee2e2; border-color: #dc2626; }
|
||||
.obs-option.disabled { pointer-events: none; opacity: 0.6; }
|
||||
/* Falsch gewählte Option bleibt gesperrt sichtbar (nicht ausgraut wie „disabled"),
|
||||
die richtige wird NICHT verraten — man wählt einfach weiter. */
|
||||
.obs-option.spent { pointer-events: none; background: #fee2e2; border-color: #fca5a5; color: #991b1b; opacity: 0.7; }
|
||||
#obs-answer-hint { margin-top: 10px; font-size: 13px; font-weight: 600; color: #dc2626; min-height: 18px; line-height: 1.4; }
|
||||
/* Schwierigkeits-Badge (nutzt das level-Feld aus observations.json) */
|
||||
.obs-badge { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: .02em;
|
||||
padding: 2px 9px; border-radius: 999px; margin-left: 8px; vertical-align: middle; white-space: nowrap; }
|
||||
.obs-badge.l1 { background: #dcfce7; color: #15803d; }
|
||||
.obs-badge.l2 { background: #dbeafe; color: #1d4ed8; }
|
||||
.obs-badge.l3 { background: #fef3c7; color: #b45309; }
|
||||
.obs-badge.l4 { background: #fce7f3; color: #be185d; }
|
||||
|
||||
#obs-sim-hint-wrap {
|
||||
margin-top: 14px;
|
||||
@@ -972,7 +983,7 @@
|
||||
<div id="obs-card">
|
||||
<button id="obs-close" type="button">✕</button>
|
||||
<div id="obs-step-intro">
|
||||
<div id="obs-progress">Aufgabe <span id="obs-num">1</span> / <span id="obs-total">20</span></div>
|
||||
<div id="obs-progress">Aufgabe <span id="obs-num">1</span> / <span id="obs-total">20</span><span id="obs-level-badge" class="obs-badge"></span></div>
|
||||
<div id="obs-blocks"></div>
|
||||
<h2 id="obs-title">Titel</h2>
|
||||
<p id="obs-intro">Hinführung</p>
|
||||
@@ -996,6 +1007,7 @@
|
||||
<div id="obs-step-answer" class="obs-step-hidden">
|
||||
<h2 id="obs-question">Frage</h2>
|
||||
<div id="obs-options"></div>
|
||||
<div id="obs-answer-hint"></div>
|
||||
</div>
|
||||
|
||||
<div id="obs-step-explain" class="obs-step-hidden">
|
||||
@@ -1013,7 +1025,8 @@
|
||||
|
||||
<div id="obs-step-done" class="obs-step-hidden">
|
||||
<h2>🎉 Geschafft!</h2>
|
||||
<p>Du hast <span id="obs-score">0</span> von <span id="obs-max">0</span> Punkten erreicht.</p>
|
||||
<p>Auf Anhieb richtig: <span id="obs-score">0</span> von <span id="obs-max">0</span> Aufgaben.</p>
|
||||
<p id="obs-firsttry-note" class="obs-small" style="color:#475569;margin-top:-6px"></p>
|
||||
<div id="snEndStars" class="sn-end-stars"></div>
|
||||
<div id="snEndAch" class="sn-end-ach" hidden></div>
|
||||
<div class="obs-actions">
|
||||
@@ -3452,6 +3465,18 @@ function loadCompleted() {
|
||||
function saveCompleted(set) {
|
||||
try { localStorage.setItem(OBS_STORAGE_KEY, JSON.stringify([...set])); } catch (e) {}
|
||||
}
|
||||
// „Auf Anhieb richtig" (Erst-Treffer) — persistiert wie completed, damit die
|
||||
// Bewertung über mehrere Sitzungen hinweg gilt. Das ist die Basis der Sterne.
|
||||
const OBS_FIRSTTRY_KEY = 'sonnensystem_obs_firsttry';
|
||||
function loadFirstTry() {
|
||||
try {
|
||||
const raw = localStorage.getItem(OBS_FIRSTTRY_KEY);
|
||||
return new Set(raw ? JSON.parse(raw) : []);
|
||||
} catch (e) { return new Set(); }
|
||||
}
|
||||
function saveFirstTry(set) {
|
||||
try { localStorage.setItem(OBS_FIRSTTRY_KEY, JSON.stringify([...set])); } catch (e) {}
|
||||
}
|
||||
const obsState = {
|
||||
tasks: [],
|
||||
index: 0,
|
||||
@@ -3461,6 +3486,7 @@ const obsState = {
|
||||
phaseTimer: null,
|
||||
stopAtCheck: null,
|
||||
completed: loadCompleted(),
|
||||
firstTry: loadFirstTry(),
|
||||
baseUrl: (window.SONNENSYSTEM_BASE || './') + 'data/observations.json',
|
||||
};
|
||||
|
||||
@@ -3600,6 +3626,14 @@ function obsShowTask(idx) {
|
||||
document.getElementById('obs-total').textContent = obsState.tasks.length;
|
||||
document.getElementById('obs-title').textContent = t.title;
|
||||
document.getElementById('obs-intro').textContent = t.intro;
|
||||
// Schwierigkeits-Badge (nutzt das level-Feld)
|
||||
const lvBadge = document.getElementById('obs-level-badge');
|
||||
if (lvBadge) {
|
||||
const lv = Math.min(4, Math.max(1, t.level || 1));
|
||||
const lvNames = { 1: 'Grundlagen', 2: 'Aufbau', 3: 'Vertiefung', 4: 'Profi' };
|
||||
lvBadge.textContent = '★'.repeat(lv) + ' ' + lvNames[lv];
|
||||
lvBadge.className = 'obs-badge l' + lv;
|
||||
}
|
||||
obsRenderBlocks();
|
||||
obsShowStep('intro');
|
||||
// „Zurück zur letzten Übung" bei erster Aufgabe ausgrauen
|
||||
@@ -3745,37 +3779,55 @@ function obsAnswered(displayIdx) {
|
||||
const correctDisplay = (typeof t._shuffledCorrect === 'number')
|
||||
? t._shuffledCorrect : t.correctIndex;
|
||||
const opts = document.querySelectorAll('.obs-option');
|
||||
opts.forEach((b, i) => {
|
||||
b.classList.add('disabled');
|
||||
if (i === correctDisplay) b.classList.add('correct');
|
||||
else if (i === displayIdx) b.classList.add('wrong');
|
||||
});
|
||||
const wasFirstTry = !obsState._attempted || !obsState._attempted[t.id];
|
||||
const clicked = opts[displayIdx];
|
||||
if (clicked && clicked.classList.contains('spent')) return; // schon verbraucht
|
||||
obsState._attempted = obsState._attempted || {};
|
||||
obsState._attempted[t.id] = (obsState._attempted[t.id] || 0) + 1;
|
||||
if (displayIdx === correctDisplay) {
|
||||
const wasNew = !obsState.completed.has(t.id);
|
||||
obsState.score += 1;
|
||||
obsState.completed.add(t.id);
|
||||
saveCompleted(obsState.completed);
|
||||
// Achievement-Auswertung pro Antwort
|
||||
if (window.snEvaluateAchievementsOnAnswer) {
|
||||
var ctx = (t.startState || {});
|
||||
var newly = window.snEvaluateAchievementsOnAnswer(
|
||||
t.id, wasFirstTry, obsState.completed.size, obsState.tasks.length, ctx
|
||||
);
|
||||
obsState._newAchievements = (obsState._newAchievements || []).concat(newly);
|
||||
}
|
||||
// Submit pro gelöster Aufgabe (live ans Backend, ohne Sterne — Set-Submit kommt am Ende)
|
||||
if (wasNew && window.snSubmitProgress) {
|
||||
window.snSubmitProgress({
|
||||
stars: 0,
|
||||
durationMs: 0,
|
||||
data: { event:'task_solved', taskId:t.id, taskTitle:t.title,
|
||||
completed:obsState.completed.size, total:obsState.tasks.length,
|
||||
firstTry: wasFirstTry }
|
||||
});
|
||||
}
|
||||
const priorTries = obsState._attempted[t.id] || 0;
|
||||
obsState._attempted[t.id] = priorTries + 1;
|
||||
const hintEl = document.getElementById('obs-answer-hint');
|
||||
|
||||
if (displayIdx !== correctDisplay) {
|
||||
// FALSCH — die richtige Lösung NICHT verraten. Nur diese Option sperren,
|
||||
// Bearbeiter:in wählt weiter. Damit zählt die Bewertung echte Erst-Treffer.
|
||||
if (clicked) { clicked.classList.remove('correct'); clicked.classList.add('spent'); }
|
||||
const nudges = [
|
||||
'Das war noch nicht richtig — denk an das, was du gerade beobachtet hast, und versuch es nochmal.',
|
||||
'Noch nicht ganz — überleg in Ruhe und wähle eine andere Antwort.',
|
||||
'Knapp daneben — probier es weiter.'
|
||||
];
|
||||
if (hintEl) hintEl.textContent = nudges[Math.min(priorTries, nudges.length - 1)];
|
||||
return;
|
||||
}
|
||||
|
||||
// RICHTIG
|
||||
opts.forEach((b) => { b.classList.add('disabled'); });
|
||||
if (clicked) clicked.classList.add('correct');
|
||||
if (hintEl) hintEl.textContent = '';
|
||||
const wasFirstTry = priorTries === 0; // beim allerersten Versuch dieser Aufgabe richtig
|
||||
const wasNew = !obsState.completed.has(t.id);
|
||||
obsState.completed.add(t.id);
|
||||
saveCompleted(obsState.completed);
|
||||
if (wasFirstTry) {
|
||||
obsState.firstTry.add(t.id);
|
||||
saveFirstTry(obsState.firstTry);
|
||||
}
|
||||
// Achievement-Auswertung pro Antwort
|
||||
if (window.snEvaluateAchievementsOnAnswer) {
|
||||
var ctx = (t.startState || {});
|
||||
var newly = window.snEvaluateAchievementsOnAnswer(
|
||||
t.id, wasFirstTry, obsState.completed.size, obsState.tasks.length, ctx
|
||||
);
|
||||
obsState._newAchievements = (obsState._newAchievements || []).concat(newly);
|
||||
}
|
||||
// Submit pro gelöster Aufgabe (live ans Backend, ohne Sterne — Set-Submit kommt am Ende)
|
||||
if (wasNew && window.snSubmitProgress) {
|
||||
window.snSubmitProgress({
|
||||
stars: 0,
|
||||
durationMs: 0,
|
||||
data: { event:'task_solved', taskId:t.id, taskTitle:t.title,
|
||||
completed:obsState.completed.size, total:obsState.tasks.length,
|
||||
firstTry: wasFirstTry }
|
||||
});
|
||||
}
|
||||
setTimeout(() => obsShowExplain(), 700);
|
||||
}
|
||||
@@ -3792,13 +3844,22 @@ function obsShowExplain() {
|
||||
|
||||
function obsShowEnd() {
|
||||
document.body.classList.remove('obs-watching');
|
||||
document.getElementById('obs-score').textContent = obsState.score;
|
||||
document.getElementById('obs-max').textContent = obsState.tasks.length;
|
||||
// Sterne aus completed/total
|
||||
const total = obsState.tasks.length || 1;
|
||||
const done = obsState.completed.size;
|
||||
const quote = done / total;
|
||||
const stars = quote >= 1.00 ? 5 : quote >= 0.85 ? 4 : quote >= 0.65 ? 3 : quote >= 0.4 ? 2 : 1;
|
||||
const total = obsState.tasks.length || 1;
|
||||
const done = obsState.completed.size;
|
||||
// Nur Erst-Treffer der TATSÄCHLICH gelösten Aufgaben zählen (robust gegen Alt-Stände).
|
||||
let firstTryN = 0;
|
||||
obsState.tasks.forEach(t => { if (obsState.firstTry.has(t.id)) firstTryN++; });
|
||||
const quote = firstTryN / total; // Bewertung = Meisterschaft auf Anhieb
|
||||
// Sterne aus der Erst-Treffer-Quote (nicht mehr aus completed → kein garantiertes 5★)
|
||||
const stars = quote >= 0.85 ? 5 : quote >= 0.68 ? 4 : quote >= 0.50 ? 3 : quote >= 0.30 ? 2 : 1;
|
||||
document.getElementById('obs-score').textContent = firstTryN;
|
||||
document.getElementById('obs-max').textContent = total;
|
||||
const ftNote = document.getElementById('obs-firsttry-note');
|
||||
if (ftNote) {
|
||||
ftNote.textContent = (done >= total)
|
||||
? 'Alle ' + total + ' Aufgaben gelöst. Die Sterne zählen, wie viele du beim ersten Versuch getroffen hast.'
|
||||
: (done + ' von ' + total + ' Aufgaben gelöst.');
|
||||
}
|
||||
const starsEl = document.getElementById('snEndStars');
|
||||
if (starsEl) {
|
||||
starsEl.innerHTML = '★'.repeat(stars) + '<span class="empty">' + '★'.repeat(5-stars) + '</span>';
|
||||
@@ -3826,7 +3887,10 @@ function obsShowEnd() {
|
||||
data: {
|
||||
event: 'set_complete',
|
||||
completed: done, total: total,
|
||||
firstTryCount: firstTryN,
|
||||
firstTryQuote: Math.round(quote*100),
|
||||
scoreQuote: Math.round(quote*100),
|
||||
firstTry: (firstTryN >= total), // legacy: alle auf Anhieb
|
||||
stars: stars,
|
||||
attemptsByTask: obsState._attempted || {},
|
||||
newAchievements: newly
|
||||
@@ -3900,6 +3964,9 @@ document.getElementById('obs-next').addEventListener('click', () => {
|
||||
document.getElementById('obs-restart').addEventListener('click', () => {
|
||||
obsState.completed.clear();
|
||||
saveCompleted(obsState.completed);
|
||||
obsState.firstTry.clear();
|
||||
saveFirstTry(obsState.firstTry);
|
||||
obsState._attempted = {};
|
||||
obsState.score = 0;
|
||||
obsShowTask(0);
|
||||
});
|
||||
@@ -3907,6 +3974,9 @@ document.getElementById('obs-skip-restart').addEventListener('click', () => {
|
||||
if (confirm('Fortschritt zurücksetzen — alle Aufgaben wieder offen?')) {
|
||||
obsState.completed.clear();
|
||||
saveCompleted(obsState.completed);
|
||||
obsState.firstTry.clear();
|
||||
saveFirstTry(obsState.firstTry);
|
||||
obsState._attempted = {};
|
||||
obsState.score = 0;
|
||||
obsShowTask(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user