diff --git a/App/sims/busfahrt/game.html b/App/sims/busfahrt/game.html index 1983f12..24fc7c6 100644 --- a/App/sims/busfahrt/game.html +++ b/App/sims/busfahrt/game.html @@ -2378,6 +2378,7 @@ window.GGS_LIVE_STATE = function () { budget: Math.round(game.budget), topoPoints: game.score, topoMax: totalOrders * 3, + leistung_pct: (totalOrders*3)===0 ? null : Math.max(0,Math.min(100,Math.round(game.score/(totalOrders*3)*100))), wonOrders: wonCount, lostOrders: lostCount, lastCityTitle: lastCity ? lastCity.title : null, @@ -2421,6 +2422,7 @@ function submitBusfahrtTour() { totalOrders: total, quizFirstTry: quizFirstTry, eventCount: (game.eventLog || []).length, + leistung_pct: (total*3)===0 ? null : Math.max(0,Math.min(100,Math.round(game.score/(total*3)*100))), } } }) diff --git a/App/sims/energiemanager/game.html b/App/sims/energiemanager/game.html index f909065..f8db4d6 100644 --- a/App/sims/energiemanager/game.html +++ b/App/sims/energiemanager/game.html @@ -2022,6 +2022,11 @@ totalGap: Math.round(totalGap * 10) / 10, totalSurplus: Math.round(totalSurplus * 10) / 10, blackouts: blackouts, + // Einheitliche Leistungskennzahl (0–100): Quote ausgeglichener Blöcke minus + // 15 Punkte je Blackout. Nenner 0 (kein Tag gespielt) → 0. + leistung_pct: blocksTotal > 0 + ? Math.max(0, Math.min(100, Math.round(blocksBalanced / blocksTotal * 100) - 15 * blackouts)) + : 0, stars: stars, perDay: perDay }; @@ -2757,6 +2762,15 @@ try { achCount = Object.keys((emLsLoad().achievements)||{}).length; } catch (_) {} var lastSet = (currentDayIdx + 1 >= setTotal && phase === 'done'); + + // Einheitliche Leistungskennzahl (0–100): Quote ausgeglichener Blöcke über die + // bisher gespielten Tage minus 15 Punkte je Blackout. Nenner 0 (noch kein Tag + // abgeschlossen) → 0. + var emLmDenom = daysCompleted * DAY.blocks.length; + var emLeistungPct = emLmDenom > 0 + ? Math.max(0, Math.min(100, Math.round(setBalancedSoFar / emLmDenom * 100) - 15 * (setRun.blackouts || 0))) + : 0; + return { // Sim-Identität + Fortschritt setId: SETS[currentSetIdx].id, @@ -2780,6 +2794,7 @@ setDaysDone: daysCompleted, setBlocksBalanced: setBalancedSoFar, blackouts: setRun.blackouts || 0, + leistung_pct: emLeistungPct, // Cross-app Konvention health: health, healthLabel: label, diff --git a/App/sims/eu-werkstatt/game.html b/App/sims/eu-werkstatt/game.html index 4f1a685..5f86614 100644 --- a/App/sims/eu-werkstatt/game.html +++ b/App/sims/eu-werkstatt/game.html @@ -5030,6 +5030,11 @@ function showEnd() { firstTryHits: state.firstTryHits, totalAttempts: state.totalAttempts, muelleimerCount: state.muelleimerCount, + // Einheitliche Leistungskennzahl (0–100): Anteil Auf-Anhieb-Treffer minus + // 5 Punkte je Mülleimer-Wurf. Nenner 0 (keine Schritte) → 0. + leistung_pct: stepsTotal > 0 + ? Math.max(0, Math.min(100, Math.round(state.firstTryHits / stepsTotal * 100) - 5 * state.muelleimerCount)) + : 0, attemptsByStep: state.attemptsByStep, detoursByStep: state.detoursByStep, reshufflesByStep: state.reshufflesByStep, @@ -5295,6 +5300,12 @@ window.GGS_LIVE_STATE = function () { var reshufflesThisStep = (state.reshufflesByStep && state.reshufflesByStep[stepIdx]) || 0; var secondsElapsed = state.startedAt ? Math.round((Date.now() - state.startedAt) / 1000) : 0; + // Einheitliche Leistungskennzahl (0–100): Anteil Auf-Anhieb-Treffer minus + // 5 Punkte je Mülleimer-Wurf. Nenner 0 (keine Schritte) → 0. + var leistungPct = stepsTotal > 0 + ? Math.max(0, Math.min(100, Math.round((state.firstTryHits || 0) / stepsTotal * 100) - 5 * (state.muelleimerCount || 0))) + : 0; + // Health-Ampel (für die Lehrer-Live-Übersicht der nächsten Phase) var health = 'good'; if (screen === 'play') { @@ -5332,6 +5343,7 @@ window.GGS_LIVE_STATE = function () { firstTryHits: state.firstTryHits || 0, totalAttempts: state.totalAttempts || 0, muelleimerCount: state.muelleimerCount || 0, + leistung_pct: leistungPct, attemptsThisStep: attemptsThisStep, reshuffles: state.totalReshuffles || 0, reshufflesThisStep: reshufflesThisStep, diff --git a/App/sims/farmer/game.html b/App/sims/farmer/game.html index 89dea36..e40d1a3 100644 --- a/App/sims/farmer/game.html +++ b/App/sims/farmer/game.html @@ -1544,6 +1544,8 @@ async function submitAssessment(stars, score, breakdown) { countries_used: breakdown.countries.value, climates_used: breakdown.climates.value, year_reached: game.year, + // Einheitliche Leistungskennzahl (0–100): gewichtete Gesamtwertung (totalScore). + leistung_pct: Math.max(0, Math.min(100, Math.round(score))), }, action_log, }, @@ -1859,6 +1861,16 @@ window.GGS_LIVE_STATE = function () { // (siehe END_REPORT_TRIGGER_YEAR = 11). Skala bezogen auf 10 Jahre, gekappt. const targetYear = (typeof END_REPORT_TRIGGER_YEAR === 'number') ? (END_REPORT_TRIGGER_YEAR - 1) : 10; const yearProgress = Math.max(0, Math.min(100, Math.round((game.year / targetYear) * 100))); + // Einheitliche Leistungskennzahl (0–100): dieselbe gewichtete Gesamtwertung wie + // im Endbericht/Submit, live aus dem aktuellen Stand berechnet. Bricht die + // Berechnung wider Erwarten, fällt sie sicher auf die Trefferquote (hit_rate) als + // dokumentierten Proxy zurück, damit die Live-Telemetrie nie ausfällt. + let leistungPct = hitRate; // Proxy: Trefferquote + try { + leistungPct = Math.max(0, Math.min(100, Math.round(computeFinalScore().score))); + } catch (e) { + leistungPct = hitRate; // Proxy: Trefferquote + } return { simId: 'farmer', phase: 'Jahr ' + game.year + ' von ' + targetYear, @@ -1872,6 +1884,7 @@ window.GGS_LIVE_STATE = function () { hit_rate: hitRate, co2_kg: totalCo2, diseases: diseases, + leistung_pct: leistungPct, crops_used: new Set(reports.map(r => r.cropId)).size, countries: new Set(reports.filter(r => r.countryName).map(r => r.countryName)).size, timestamp: Date.now(), diff --git a/App/sims/fluggesellschaft/game.html b/App/sims/fluggesellschaft/game.html index 6b51f12..93d171a 100644 --- a/App/sims/fluggesellschaft/game.html +++ b/App/sims/fluggesellschaft/game.html @@ -708,6 +708,7 @@ function submitTourToBackend() { totalOrders: total, quizFirstTry, quizFailCount: game.reports.reduce((s,r)=>s+Math.max(0,(r.quizAttemptsCount||1)-1), 0), + leistung_pct: (total*3)===0 ? null : Math.max(0,Math.min(100,Math.round(game.score/(total*3)*100))), } } }) @@ -737,6 +738,7 @@ window.GGS_LIVE_STATE = function() { lastMargin: lastR ? lastR.margin : null, lastTzDiff: lastR ? lastR.tzDiffHours : null, quizFirstTry: reports.filter(r => r.quizFirstTry).length, + leistung_pct: (game.tourOrders.length*3)===0 ? null : Math.max(0,Math.min(100,Math.round(game.score/(game.tourOrders.length*3)*100))), timestamp: Date.now(), }; }; diff --git a/App/sims/fluss/game.html b/App/sims/fluss/game.html index 396b43f..9618f8b 100644 --- a/App/sims/fluss/game.html +++ b/App/sims/fluss/game.html @@ -1983,6 +1983,7 @@ function submitReflection(levelNum, question, answer) { if (window.flAudio) flAudio.play(summary.stars >= 3 ? 'level-won' : 'level-lost'); submitAssessment({ level: game.level, stars: summary.stars, score: summary.score, + leistung_pct: Math.max(0, Math.min(100, Math.round(summary.score))), duration_ms: 0, completed: summary.completed, results: { final_biodiv: summary.params.biodiv, final_economy: summary.params.economy, final_flood: summary.params.flood, final_food: summary.params.food, @@ -2153,6 +2154,7 @@ function submitReflection(levelNum, question, answer) { population: Math.round(p.population || 0), food: Math.round(p.food || 0), score: score, + leistung_pct: score, health: health, settlementsLost: settlementsLost, gameOver: !!st.gameOver, diff --git a/App/sims/heli/game.html b/App/sims/heli/game.html index 1edb569..01e5e3b 100644 --- a/App/sims/heli/game.html +++ b/App/sims/heli/game.html @@ -501,6 +501,18 @@ window.GGS_LIVE_STATE = function () { if (!parts.length) return null; return Math.round(parts.reduce(function(a,b){return a+b;},0)/parts.length); })(), + // Gleicher Wert wie score (0–100), als vereinheitlichte Leistungs-Kennzahl. + leistung_pct: (function(){ + if (!phase || phase === 'mission' || phase === 'briefing') return null; + if (landingDone && game.totalStars != null) return Math.round(Math.max(0,Math.min(5,game.totalStars))/5*100); + var parts = []; + var clicks = (game.planCorrect||0) + (game.planAttempts||0); + if (clicks > 0) parts.push(Math.round((game.planCorrect||0) / clicks * 100)); + if (startDone && game.startScore != null) parts.push(Math.max(0,Math.min(5,game.startScore))/5*100); + if (landingDone && game.landingScore != null) parts.push(Math.max(0,Math.min(5,game.landingScore))/5*100); + if (!parts.length) return null; + return Math.round(parts.reduce(function(a,b){return a+b;},0)/parts.length); + })(), }; }; @@ -786,6 +798,7 @@ function submitHeliMission() { level: window.HELI_FORCED_LEVEL || null, stars: stars, score: score, + leistung_pct: Math.max(0, Math.min(100, Math.round(score))), duration_ms: dt, completed: !game.missionFailed, results: { diff --git a/App/sims/klima/game-2d.html b/App/sims/klima/game-2d.html index d41398b..162d767 100644 --- a/App/sims/klima/game-2d.html +++ b/App/sims/klima/game-2d.html @@ -1333,6 +1333,7 @@ window.GGS_LIVE_STATE = function () { // Laufender Punktestand (0–100, gleiche Formel wie am Ende) → Ampelfarbe im // Lehrer-Live (Tag-Ansicht färbt laufende Balken danach ein). score: _liveKlimaScore(), + leistung_pct:_liveKlimaScore(), }; }; // Live-Score aus dem aktuellen Stand — nur während des Spiels sinnvoll. @@ -3876,6 +3877,7 @@ function endLevel(won, reason) { action: 'submit_assessment', data: { level: state.difficulty, stars, score, + leistung_pct: Math.max(0, Math.min(100, Math.round(score))), duration_ms: durationMs, completed: state.levelWon, results: { final_co2: Math.round(state.co2Ppm), diff --git a/App/sims/klima/game-3d.html b/App/sims/klima/game-3d.html index a59eee0..331725e 100644 --- a/App/sims/klima/game-3d.html +++ b/App/sims/klima/game-3d.html @@ -1573,6 +1573,7 @@ window.GGS_LIVE_STATE = function () { // Laufender Punktestand (0–100, gleiche Formel wie am Ende) → Ampelfarbe im // Lehrer-Live (Tag-Ansicht färbt laufende Balken danach ein). score: _liveKlimaScore(), + leistung_pct:_liveKlimaScore(), }; }; // Live-Score aus dem aktuellen Stand — nur während des Spiels sinnvoll. @@ -5347,6 +5348,7 @@ function endLevel(won, reason) { action: 'submit_assessment', data: { level: state.difficulty, stars, score, + leistung_pct: Math.max(0, Math.min(100, Math.round(score))), duration_ms: durationMs, completed: state.levelWon, results: { final_co2: Math.round(state.co2Ppm), diff --git a/App/sims/kofferdetektiv/engine.js b/App/sims/kofferdetektiv/engine.js index 86fc170..4f64f27 100644 --- a/App/sims/kofferdetektiv/engine.js +++ b/App/sims/kofferdetektiv/engine.js @@ -3454,6 +3454,7 @@ const Platform = { level: S.cfg.level, stars: Game.stars(), score: Game.score100(), + leistung_pct: Math.max(0, Math.min(100, Math.round(Game.score100()))), duration_ms: (window.__GGS_LIVE__ && window.__GGS_LIVE__.activeMs) || 0, completed: true, results: Game.results(), @@ -3487,6 +3488,7 @@ window.GGS_LIVE_STATE = function () { phase: phaseLabel, progressPct: Math.round((solved + (cs && cs.found && !cs.solved ? 0.5 : 0)) / S.cases.length * 100), score: Game.score100(), + leistung_pct: Game.score100(), health: wrongHere >= 3 || (cs && cs.timeoutHit && !cs.solved) ? 'struggle' : wrongHere >= 1 ? 'ok' : 'good', cases_solved: solved, cases_total: S.cases.length, diff --git a/App/sims/logistik/game.html b/App/sims/logistik/game.html index 9f08bd8..f9ee6b9 100644 --- a/App/sims/logistik/game.html +++ b/App/sims/logistik/game.html @@ -974,6 +974,7 @@ window.pickText = function(opts) { action: 'end', attemptId: activeAttemptId, finalBalance: Math.round(game.balance || 0), + leistung_pct: Math.max(0, Math.min(100, Math.round((game.balance || 0) / 16000 * 100))), success: !!success, stats: stats || {}, }; @@ -5536,6 +5537,7 @@ window.pickText = function(opts) { results: { success: success, finalBalance: Math.round(game.balance || 0), + leistung_pct: Math.max(0, Math.min(100, Math.round((game.balance || 0) / 16000 * 100))), minTarget: (game._config && game._config.minTargetEarnings) || 0, completed: a.completed, lateRatio: a.lateRatio, @@ -5572,6 +5574,7 @@ window.pickText = function(opts) { simId: 'logistik', levelId: levelNum || 1, progressPct: pct, + leistung_pct: Math.max(0, Math.min(100, Math.round((game.balance || 0) / 16000 * 100))), phase: 'Runde ' + ((game.progression && game.progression.round) || 1), balance: Math.round(game.balance || 0), ordersDelivered: delivered, diff --git a/App/sims/sonnensystem/game.html b/App/sims/sonnensystem/game.html index dcf0cb0..184d437 100644 --- a/App/sims/sonnensystem/game.html +++ b/App/sims/sonnensystem/game.html @@ -3514,6 +3514,17 @@ window.GGS_LIVE_STATE = function () { var screen = document.getElementById('obs-overlay'); var inObs = screen && !screen.classList.contains('obs-hidden'); var quote = total > 0 ? Math.round(done/total*100) : 0; + // Erst-Treffer-Quote live (bevorzugt), sonst Fortschritt + var ftLive = 0, hasFt = false; + try { + if (obsState && obsState.firstTry && obsState.tasks) { + hasFt = true; + obsState.tasks.forEach(function(t){ if (obsState.firstTry.has(t.id)) ftLive++; }); + } + } catch (_) {} + var leistungPct = (hasFt && total > 0) + ? Math.max(0, Math.min(100, Math.round(ftLive/total*100))) + : quote; var health = 'good'; if (inObs) { if (quote >= 75) health = 'good'; @@ -3535,6 +3546,7 @@ window.GGS_LIVE_STATE = function () { taskId: t ? t.id : null, completed: done, progressPct: quote, + leistung_pct: leistungPct, simDate: simDate, timeDays: dayInt, paused: (typeof paused === 'boolean') ? paused : false, @@ -3989,6 +4001,7 @@ function obsShowEnd() { completed: done, total: total, firstTryCount: firstTryN, firstTryQuote: Math.round(quote*100), + leistung_pct: Math.max(0, Math.min(100, Math.round(quote*100))), scoreQuote: Math.round(quote*100), firstTry: (firstTryN >= total), // legacy: alle auf Anhieb stars: stars, diff --git a/App/sims/tourismusregion/js/main.js b/App/sims/tourismusregion/js/main.js index 88138eb..03bf6ec 100644 --- a/App/sims/tourismusregion/js/main.js +++ b/App/sims/tourismusregion/js/main.js @@ -270,6 +270,26 @@ function pumpCards() { // Wird vom live-client.js alle ~4 s an /api/live gesendet. window.GGS_LIVE_STATE = function () { if (!state) return null; + // Einheitliche Leistungskennzahl: gewichteter Score live (gleiche Formel wie finish()), + // Fallback avgStars/5*100, falls Teil-Stats fehlen. + const _avg = currentAvg(state); + let _leistung_pct; + try { + const _st = state.stats; + const _nonCar = _st.transports.foot + _st.transports.bike + _st.transports.bus; + const _totalSp = Math.max(1, _st.spawned); + let _lenkung = (_nonCar / _totalSp) * 100; + if (state.buildings.some(b => b.type === 'bushaltestelle')) _lenkung = Math.min(100, _lenkung + 10); + const _scoreLive = (_avg / 5) * 100 * 0.35 + + Math.min(100, (state.revenueTotal / 2500) * 100) * 0.25 + + state.env * 0.20 + + _lenkung * 0.10 + + (state.budget >= 0 ? 100 : Math.max(0, 100 * (1 + state.budget / 1000))) * 0.10; + _leistung_pct = Math.round(_scoreLive); + } catch (_) { + _leistung_pct = Math.round((_avg / 5) * 100); + } + _leistung_pct = Math.max(0, Math.min(100, _leistung_pct)); return { scenarioId: state.map?.id, // Karten-Key = Lektions-Gruppierung mapId: state.map?.id, @@ -286,6 +306,7 @@ window.GGS_LIVE_STATE = function () { goalsDone: GOALS.filter(g => g.check(state)).length, goalsTotal: GOALS.length, ended: !!state.ended, + leistung_pct: _leistung_pct, }; }; @@ -311,6 +332,7 @@ function submitAssessment() { level: state.map?.id, stars: Math.max(0, Math.min(5, Math.round(r.avgStars))), score: r.score, + leistung_pct: Math.max(0, Math.min(100, Math.round(r.score))), duration_ms: Date.now() - (state._startedAt || Date.now()), completed: goalsDone === r.goals.length, results: { diff --git a/App/sims/tourismustal/js/main.js b/App/sims/tourismustal/js/main.js index f650d33..f6596fa 100644 --- a/App/sims/tourismustal/js/main.js +++ b/App/sims/tourismustal/js/main.js @@ -207,6 +207,12 @@ function onTileTap(c, r, pointerType) { // stabile Schlüssel, didaktisch sinnvoll (Cockpit-Spalten). window.GGS_LIVE_STATE = function () { if (!state) return null; + const _goalsDone = state.scenario.goals.filter(g => g.check(state)).length; + const _goalsTotal = state.scenario.goals.length; + const _goalPct = _goalsTotal > 0 ? (_goalsDone / _goalsTotal) * 100 : 0; + const _leistung_pct = Math.max(0, Math.min(100, Math.round( + 0.5 * _goalPct + 0.3 * (state.satisfaction || 0) + 0.2 * (state.nature || 0) + ))); return { scene: state.scenario.id, scenarioId: state.scenario.id, // Plattform-Konvention für Lektions-Gruppierung @@ -228,6 +234,7 @@ window.GGS_LIVE_STATE = function () { goalsTotal: state.scenario.goals.length, questsDone: state.questIndex, bottleneck: state.bottleneck, + leistung_pct: _leistung_pct, }; }; @@ -252,6 +259,10 @@ function submitAssessment(completed) { level: state.scenario.id, stars, score: Math.round((goals / total) * 100), + leistung_pct: Math.max(0, Math.min(100, Math.round( + 0.5 * (total > 0 ? (goals / total) * 100 : 0) + + 0.3 * (state.satisfaction || 0) + 0.2 * (state.nature || 0) + ))), duration_ms: Date.now() - (state._startedAt || Date.now()), completed, results: { diff --git a/App/sims/weltkueche/game.html b/App/sims/weltkueche/game.html index 4db7393..32aff3e 100644 --- a/App/sims/weltkueche/game.html +++ b/App/sims/weltkueche/game.html @@ -810,6 +810,7 @@ window.GGS_LIVE_STATE = function() { score: state.score || 0, correctClicks: state.totalCorrect || 0, wrongClicks: state.totalWrong || 0, + leistung_pct: ((state.totalCorrect||0)+(state.totalWrong||0))===0 ? null : Math.max(0,Math.min(100,Math.round((state.totalCorrect||0)/((state.totalCorrect||0)+(state.totalWrong||0))*100))), completed: completedDish, completedTotal: totalDish, badgesEarned: (state.badges||[]).length, @@ -849,6 +850,7 @@ function wkSubmitProgress(dish, perfect){ completed: state.completed.length, total: DISHES.length, score: state.score, dishScore: (current && current.dishScore) || 0, correctClicks: state.totalCorrect || 0, wrongClicks: state.totalWrong || 0, + leistung_pct: ((state.totalCorrect||0)+(state.totalWrong||0))===0 ? null : Math.max(0,Math.min(100,Math.round((state.totalCorrect||0)/((state.totalCorrect||0)+(state.totalWrong||0))*100))), mistakesInDish: mis, perfect: !!perfect } })