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 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
|
||||||
await initForestScreen(db, startLessonFromForest);
|
showScreen("reward");
|
||||||
showScreen("forest");
|
|
||||||
|
// 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> {
|
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");
|
||||||
|
|||||||
Reference in New Issue
Block a user