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:
+98
-9
@@ -1,7 +1,8 @@
|
||||
import type { ScreenName } from "./types";
|
||||
import { openDB, getProgress, saveProgress } from "./storage/db";
|
||||
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 { companions } from "./companion/characters";
|
||||
import { showParentCodePrompt, initParentScreen } from "./ui/parent";
|
||||
@@ -25,10 +26,44 @@ export function getDB(): IDBDatabase {
|
||||
|
||||
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
|
||||
const progress = await getProgress(db);
|
||||
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
|
||||
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)
|
||||
// Cap at MAX_LEVEL + 1 so we don't go beyond defined levels
|
||||
if (completedLevel === progress.currentLevel && completedLevel < MAX_LEVEL) {
|
||||
// Cap at MAX_LEVEL so we don't go beyond defined levels
|
||||
if (
|
||||
completedLevel === progress.currentLevel &&
|
||||
completedLevel < MAX_LEVEL
|
||||
) {
|
||||
progress.currentLevel = completedLevel + 1;
|
||||
}
|
||||
|
||||
@@ -52,9 +90,38 @@ async function handleLessonComplete(completedLevel: number): Promise<void> {
|
||||
|
||||
await saveProgress(db, progress);
|
||||
|
||||
// Re-init forest screen with updated progress, then navigate
|
||||
await initForestScreen(db, startLessonFromForest);
|
||||
showScreen("forest");
|
||||
// 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 renderForestScene(db, elementId);
|
||||
showScreen("forest");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function handleLessonCancel(): Promise<void> {
|
||||
@@ -69,9 +136,27 @@ function startLessonFromForest(level: number): void {
|
||||
lessonCleanup = null;
|
||||
}
|
||||
|
||||
// Get layout from progress (already loaded)
|
||||
// Get layout and determine review/encouragement from 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");
|
||||
|
||||
@@ -81,6 +166,8 @@ function startLessonFromForest(level: number): void {
|
||||
layout,
|
||||
handleLessonComplete,
|
||||
handleLessonCancel,
|
||||
isReview,
|
||||
showEncouragement,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -104,6 +191,8 @@ export async function initApp(): Promise<void> {
|
||||
|
||||
const progress = await getProgress(db);
|
||||
if (progress && progress.selectedCharacter) {
|
||||
migrateProgress(progress);
|
||||
|
||||
// Returning user -- init forest and go there
|
||||
await initForestScreen(db, startLessonFromForest);
|
||||
showScreen("forest");
|
||||
|
||||
Reference in New Issue
Block a user