feat(03-04): wire complete game flow with reward screen and review triggers

- Lesson completion now shows reward screen instead of going directly to forest
- Pre-generation starts at lesson begin (fire-and-forget)
- Stats accumulated (correct/errors/errorKeys) in progress after each lesson
- Review mode triggers every 3rd session
- Encouragement shown after 2x same level (repetition protection)
- Progress migration for older saved data without new fields
This commit is contained in:
2026-03-29 13:53:23 +02:00
parent e599050303
commit aaf9872f1d
+96 -7
View File
@@ -1,7 +1,8 @@
import type { ScreenName } from "./types"; import type { ScreenName } from "./types";
import { openDB, getProgress, saveProgress } from "./storage/db"; import { openDB, getProgress, saveProgress } from "./storage/db";
import { initWelcomeScreen, initLessonScreen } from "./ui/screens"; import { initWelcomeScreen, initLessonScreen } from "./ui/screens";
import { initForestScreen } from "./forest/scene"; import { initForestScreen, renderForestScene } from "./forest/scene";
import { startPreGeneration, initRewardScreen } from "./forest/reward";
import { getGreeting } from "./companion/companion"; import { getGreeting } from "./companion/companion";
import { companions } from "./companion/characters"; import { companions } from "./companion/characters";
import { showParentCodePrompt, initParentScreen } from "./ui/parent"; import { showParentCodePrompt, initParentScreen } from "./ui/parent";
@@ -25,10 +26,44 @@ export function getDB(): IDBDatabase {
const MAX_LEVEL = 6; const MAX_LEVEL = 6;
async function handleLessonComplete(completedLevel: number): Promise<void> { /**
* Ensure progress object has all fields (migration for older saved data).
*/
function migrateProgress(
progress: NonNullable<Awaited<ReturnType<typeof getProgress>>>,
): void {
if (!progress.errorKeyCounts) progress.errorKeyCounts = {};
if (!progress.lastPlayedLevels) progress.lastPlayedLevels = [];
if (progress.totalCorrect === undefined) progress.totalCorrect = 0;
if (progress.totalErrors === undefined) progress.totalErrors = 0;
}
async function handleLessonComplete(
completedLevel: number,
stats: {
totalCorrect: number;
totalErrors: number;
errorKeys: Record<string, number>;
},
): Promise<void> {
// Update progress after lesson completion // Update progress after lesson completion
const progress = await getProgress(db); const progress = await getProgress(db);
if (!progress) return; if (!progress) return;
migrateProgress(progress);
// Accumulate exercise stats (D-15, D-16)
progress.totalCorrect += stats.totalCorrect;
progress.totalErrors += stats.totalErrors;
for (const [key, count] of Object.entries(stats.errorKeys)) {
progress.errorKeyCounts[key] =
(progress.errorKeyCounts[key] ?? 0) + count;
}
// Track last played levels (keep last 3) for repetition detection
progress.lastPlayedLevels.push(completedLevel);
if (progress.lastPlayedLevels.length > 3) {
progress.lastPlayedLevels = progress.lastPlayedLevels.slice(-3);
}
// Add level to completedLevels if not already there // Add level to completedLevels if not already there
if (!progress.completedLevels.includes(completedLevel)) { if (!progress.completedLevels.includes(completedLevel)) {
@@ -36,8 +71,11 @@ async function handleLessonComplete(completedLevel: number): Promise<void> {
} }
// Advance to next level if this was the current level (LEVL-02, LEVL-03) // Advance to next level if this was the current level (LEVL-02, LEVL-03)
// Cap at MAX_LEVEL + 1 so we don't go beyond defined levels // Cap at MAX_LEVEL so we don't go beyond defined levels
if (completedLevel === progress.currentLevel && completedLevel < MAX_LEVEL) { if (
completedLevel === progress.currentLevel &&
completedLevel < MAX_LEVEL
) {
progress.currentLevel = completedLevel + 1; progress.currentLevel = completedLevel + 1;
} }
@@ -52,9 +90,38 @@ async function handleLessonComplete(completedLevel: number): Promise<void> {
await saveProgress(db, progress); await saveProgress(db, progress);
// Re-init forest screen with updated progress, then navigate // Show REWARD screen instead of going directly to forest
showScreen("reward");
// Set companion avatar on reward screen
const companion = companions.find(
(c) => c.type === progress.selectedCharacter,
);
if (companion) {
const avatar = document.getElementById(
"reward-companion-avatar",
) as HTMLImageElement;
avatar.src = companion.avatarUrl;
avatar.alt = companion.name;
}
const elementId = await initRewardScreen(
db,
completedLevel,
progress.selectedCharacter,
() => {
// "Weiter ueben" -- start next lesson
startLessonFromForest(
Math.min(progress.currentLevel, MAX_LEVEL),
);
},
async () => {
// "Zurueck zum Wald" -- show forest with new element animated
await initForestScreen(db, startLessonFromForest); await initForestScreen(db, startLessonFromForest);
await renderForestScene(db, elementId);
showScreen("forest"); showScreen("forest");
},
);
} }
async function handleLessonCancel(): Promise<void> { async function handleLessonCancel(): Promise<void> {
@@ -69,9 +136,27 @@ function startLessonFromForest(level: number): void {
lessonCleanup = null; lessonCleanup = null;
} }
// Get layout from progress (already loaded) // Get layout and determine review/encouragement from progress
getProgress(db).then((progress) => { getProgress(db).then((progress) => {
const layout = progress?.selectedLayout ?? "de"; if (!progress) return;
migrateProgress(progress);
const layout = progress.selectedLayout ?? "de";
// Review every 3rd session (D-05)
const isReview =
progress.totalSessions > 0 &&
progress.totalSessions % 3 === 0;
// Encouragement if last 2 played levels are the same as current (D-06)
const last2 = progress.lastPlayedLevels.slice(-2);
const showEncouragement =
last2.length === 2 &&
last2[0] === level &&
last2[1] === level;
// Start image pre-generation (D-11) -- fire and forget
startPreGeneration(db);
showScreen("lesson"); showScreen("lesson");
@@ -81,6 +166,8 @@ function startLessonFromForest(level: number): void {
layout, layout,
handleLessonComplete, handleLessonComplete,
handleLessonCancel, handleLessonCancel,
isReview,
showEncouragement,
); );
}); });
} }
@@ -104,6 +191,8 @@ export async function initApp(): Promise<void> {
const progress = await getProgress(db); const progress = await getProgress(db);
if (progress && progress.selectedCharacter) { if (progress && progress.selectedCharacter) {
migrateProgress(progress);
// Returning user -- init forest and go there // Returning user -- init forest and go there
await initForestScreen(db, startLessonFromForest); await initForestScreen(db, startLessonFromForest);
showScreen("forest"); showScreen("forest");