Per-Sim leistung_pct: alle 14 Sims senden das eine Leistungs-% (0-100)

Jede Sim liefert jetzt selbst leistung_pct in GGS_LIVE_STATE UND im Submit
(Fundament fuer das einheitliche %-System) — kein Rueckrechnen aus Sternen
mehr, keine 0-3-Sonderfaelle. Formeln je Sim wie vereinbart:
- klima/fluss/kofferdetektiv/heli: = vorhandener 0-100-score
- weltkueche: richtige/(richtige+falsche)*100 (Fehler in %)
- busfahrt/fluggesellschaft: topoPunkte/(Auftraege*3)*100
- farmer: gewichteter totalScore; energiemanager: ausgegl./gesamt -15/Blackout
- eu-werkstatt: firstTry/Schritte -5/Muelleimer; sonnensystem: Erstversuch-Quote
- tourismustal: Ziele+Zufriedenheit+Natur gewichtet; tourismusregion: gewichteter score
- logistik: Kontostand->% (sendet erstmals eine Kennzahl, auch an /api/assessment)
Teacher _leistungPct + live.php extractScore bevorzugen bereits diesen Wert.
Additive Aenderungen, bestehende Logik/Sterne unveraendert; alle 15 Dateien
syntaktisch geprueft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 12:35:56 +02:00
parent 539f6b012e
commit 88e3c09e5c
15 changed files with 116 additions and 0 deletions
+2
View File
@@ -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))),
}
}
})
+15
View File
@@ -2022,6 +2022,11 @@
totalGap: Math.round(totalGap * 10) / 10,
totalSurplus: Math.round(totalSurplus * 10) / 10,
blackouts: blackouts,
// Einheitliche Leistungskennzahl (0100): 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 (0100): 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,
+12
View File
@@ -5030,6 +5030,11 @@ function showEnd() {
firstTryHits: state.firstTryHits,
totalAttempts: state.totalAttempts,
muelleimerCount: state.muelleimerCount,
// Einheitliche Leistungskennzahl (0100): 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 (0100): 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,
+13
View File
@@ -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 (0100): 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 (0100): 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(),
+2
View File
@@ -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(),
};
};
+2
View File
@@ -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,
+13
View File
@@ -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 (0100), 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: {
+2
View File
@@ -1333,6 +1333,7 @@ window.GGS_LIVE_STATE = function () {
// Laufender Punktestand (0100, 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),
+2
View File
@@ -1573,6 +1573,7 @@ window.GGS_LIVE_STATE = function () {
// Laufender Punktestand (0100, 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),
+2
View File
@@ -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,
+3
View File
@@ -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,
+13
View File
@@ -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,
+22
View File
@@ -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: {
+11
View File
@@ -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: {
+2
View File
@@ -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
}
})