import { speakText } from "./audio/speech"; import { companions } from "./companion/characters"; import { getGreeting } from "./companion/companion"; import { initRewardScreen, startPreGeneration } from "./forest/reward"; import { initForestScreen, renderForestScene } from "./forest/scene"; import { getProgress, openDB, saveProgress } from "./storage/db"; import type { ScreenName } from "./types"; import { initParentScreen, showParentCodePrompt } from "./ui/parent"; import { initLessonScreen, initWelcomeScreen } from "./ui/screens"; let db: IDBDatabase; let lessonCleanup: (() => void) | null = null; export function showScreen(name: ScreenName): void { document.querySelectorAll(".screen").forEach((el) => { el.classList.remove("screen--active"); }); const target = document.getElementById(`screen-${name}`); if (target) { target.classList.add("screen--active"); } } export function getDB(): IDBDatabase { return db; } const MAX_LEVEL = 6; /** * Ensure progress object has all fields (migration for older saved data). */ function migrateProgress( progress: NonNullable>>, ): 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; }, ): Promise { // 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)) { progress.completedLevels.push(completedLevel); } // Advance to next level if this was the current level (LEVL-02, LEVL-03) // Cap at MAX_LEVEL so we don't go beyond defined levels if ( completedLevel === progress.currentLevel && completedLevel < MAX_LEVEL ) { progress.currentLevel = completedLevel + 1; } // Increment session count progress.totalSessions++; // Add today's date if not already recorded const today = new Date().toISOString().split("T")[0]!; if (!progress.sessionDates.includes(today)) { progress.sessionDates.push(today); } await saveProgress(db, progress); // 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 { await initForestScreen(db, startLessonFromForest); showScreen("forest"); } function startLessonFromForest(level: number): void { // Clean up previous lesson if any if (lessonCleanup) { lessonCleanup(); lessonCleanup = null; } // Get layout and determine review/encouragement from progress getProgress(db).then((progress) => { 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"); lessonCleanup = initLessonScreen( db, level, layout, handleLessonComplete, handleLessonCancel, isReview, showEncouragement, ); }); } export async function initApp(): Promise { db = await openDB(); // Wire parent screen back button initParentScreen(db, async () => { await initForestScreen(db, startLessonFromForest); showScreen("forest"); }); // Global keyboard shortcut for parent area (Ctrl+Shift+E) document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.shiftKey && e.key === "E") { e.preventDefault(); showParentCodePrompt(); } }); const progress = await getProgress(db); if (progress && progress.selectedCharacter) { migrateProgress(progress); // Returning user -- init forest and go there await initForestScreen(db, startLessonFromForest); showScreen("forest"); // Show companion greeting (per COMP-01, D-14) const companion = companions.find( (c) => c.type === progress.selectedCharacter, ); if (companion) { const greetingEl = document.getElementById("forest-greeting")!; const avatarEl = document.getElementById( "forest-greeting-avatar", ) as HTMLImageElement; const textEl = document.getElementById("forest-greeting-text")!; avatarEl.src = companion.avatarUrl; avatarEl.alt = companion.name; greetingEl.style.display = "flex"; // Fetch greeting async (shows immediately with avatar, text fills in) getGreeting(progress.selectedCharacter, db).then((text) => { textEl.textContent = text; speakText(text); }); } } else { // First time -- show welcome, then forest after setup initWelcomeScreen(db, async (screen: ScreenName) => { showScreen(screen); if (screen === "forest") { await initForestScreen(db, startLessonFromForest); } }); showScreen("welcome"); } }