diff --git a/src/app.ts b/src/app.ts
index c391b3d..c8cebb0 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,6 +1,7 @@
-import type { ScreenName, KeyboardLayout } from "./types";
+import type { ScreenName } from "./types";
import { openDB, getProgress, saveProgress } from "./storage/db";
import { initWelcomeScreen, initLessonScreen } from "./ui/screens";
+import { initForestScreen } from "./forest/scene";
let db: IDBDatabase;
let lessonCleanup: (() => void) | null = null;
@@ -19,46 +20,56 @@ export function getDB(): IDBDatabase {
return db;
}
-export async function startLesson(
- level: number,
- layout: KeyboardLayout,
-): Promise {
+async function handleLessonComplete(completedLevel: number): Promise {
+ // Update progress after lesson completion
+ const progress = await getProgress(db);
+ if (!progress) return;
+
+ // 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)
+ if (completedLevel === progress.currentLevel) {
+ 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);
+
+ // Re-init forest screen with updated progress, then navigate
+ await initForestScreen(db, startLessonFromForest);
+ showScreen("forest");
+}
+
+function startLessonFromForest(level: number): void {
// Clean up previous lesson if any
if (lessonCleanup) {
lessonCleanup();
lessonCleanup = null;
}
- showScreen("lesson");
+ // Get layout from progress (already loaded)
+ getProgress(db).then((progress) => {
+ const layout = progress?.selectedLayout ?? "de";
- lessonCleanup = initLessonScreen(db, level, layout, async (completedLevel: number) => {
- // Update progress after lesson completion
- const progress = await getProgress(db);
- if (!progress) return;
+ showScreen("lesson");
- // 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)
- if (completedLevel === progress.currentLevel) {
- 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);
-
- // Navigate to forest (per D-07: skip reward in Phase 1)
- showScreen("forest");
+ lessonCleanup = initLessonScreen(
+ db,
+ level,
+ layout,
+ handleLessonComplete,
+ );
});
}
@@ -66,11 +77,17 @@ export async function initApp(): Promise {
db = await openDB();
const progress = await getProgress(db);
if (progress && progress.selectedCharacter) {
- // Returning user -- go to forest
+ // Returning user -- init forest and go there
+ await initForestScreen(db, startLessonFromForest);
showScreen("forest");
} else {
- // First time -- show welcome
- initWelcomeScreen(db, showScreen);
+ // First time -- show welcome, then forest after setup
+ initWelcomeScreen(db, async (screen: ScreenName) => {
+ showScreen(screen);
+ if (screen === "forest") {
+ await initForestScreen(db, startLessonFromForest);
+ }
+ });
showScreen("welcome");
}
}
diff --git a/src/forest/scene.ts b/src/forest/scene.ts
new file mode 100644
index 0000000..97661e6
--- /dev/null
+++ b/src/forest/scene.ts
@@ -0,0 +1,66 @@
+import type { Progress, LevelStatus } from "../types";
+import { levels } from "../game/levels";
+import { getProgress } from "../storage/db";
+
+export function getLevelStatus(
+ levelNum: number,
+ progress: Progress,
+): LevelStatus {
+ if (progress.completedLevels.includes(levelNum)) return "completed";
+ if (levelNum === progress.currentLevel) return "current";
+ return "locked";
+}
+
+export function renderLevelMap(
+ container: HTMLElement,
+ progress: Progress,
+ onLevelClick: (level: number) => void,
+): void {
+ container.innerHTML = "";
+
+ levels.forEach((level) => {
+ const status = getLevelStatus(level.level, progress);
+ const card = document.createElement("button");
+ card.className = `level-card level-card--${status}`;
+ card.dataset.level = String(level.level);
+
+ const statusIcon =
+ status === "completed" ? "\u2713" : status === "current" ? "\u2192" : "\uD83D\uDD12";
+ const keysDisplay = level.newKeys
+ .filter((k) => k !== " ")
+ .map((k) => k.toUpperCase())
+ .join(" ");
+
+ card.innerHTML = `
+ Stufe ${level.level}
+ ${keysDisplay}${level.newKeys.includes(" ") ? " + Leertaste" : ""}
+ ${statusIcon}
+ `;
+
+ // Only current and completed levels are clickable
+ if (status !== "locked") {
+ card.addEventListener("click", () => onLevelClick(level.level));
+ } else {
+ card.disabled = true;
+ }
+
+ container.appendChild(card);
+ });
+}
+
+export async function initForestScreen(
+ db: IDBDatabase,
+ onStartLesson: (level: number) => void,
+): Promise {
+ const mapContainer = document.getElementById("level-map")!;
+ const continueBtn = document.getElementById(
+ "continue-btn",
+ ) as HTMLButtonElement;
+ const progress = await getProgress(db);
+ if (!progress) return;
+
+ renderLevelMap(mapContainer, progress, onStartLesson);
+
+ // Continue button starts the current level
+ continueBtn.onclick = () => onStartLesson(progress.currentLevel);
+}
diff --git a/src/styles/main.css b/src/styles/main.css
index e07486e..0a0a08c 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -162,6 +162,103 @@ h3 {
cursor: not-allowed;
}
+/* Forest screen */
+.forest {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 2rem;
+ min-height: 100vh;
+ background: var(--bg-forest);
+}
+
+.forest__title {
+ font-family: "Quicksand", sans-serif;
+ font-size: 2rem;
+ color: var(--text-dark);
+ margin-bottom: 1.5rem;
+}
+
+.forest__level-map {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+ max-width: 500px;
+ width: 100%;
+ margin-bottom: 2rem;
+}
+
+.level-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 1rem;
+ border-radius: 12px;
+ border: 2px solid transparent;
+ font-family: "Nunito", sans-serif;
+ cursor: pointer;
+ transition:
+ transform 0.2s ease,
+ border-color 0.2s ease;
+ background: white;
+}
+
+.level-card:hover:not(:disabled) {
+ transform: scale(1.05);
+}
+
+.level-card--current {
+ border-color: var(--accent-gold);
+ background: #fff8e0;
+ box-shadow: 0 2px 8px rgba(232, 184, 75, 0.3);
+}
+
+.level-card--completed {
+ border-color: var(--accent-green);
+ background: #e8f5e4;
+}
+
+.level-card--locked {
+ opacity: 0.5;
+ cursor: not-allowed;
+ background: #f0f0f0;
+}
+
+.level-card__number {
+ font-weight: 700;
+ font-size: 0.9rem;
+ color: var(--text-dark);
+}
+
+.level-card__keys {
+ font-family: "Quicksand", sans-serif;
+ font-weight: 600;
+ font-size: 1.1rem;
+ color: var(--accent-purple);
+}
+
+.level-card__status {
+ font-size: 1.2rem;
+}
+
+.forest__continue-btn {
+ padding: 1rem 3rem;
+ border-radius: 12px;
+ background: var(--accent-green);
+ color: white;
+ border: none;
+ font-family: "Quicksand", sans-serif;
+ font-weight: 700;
+ font-size: 1.3rem;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.forest__continue-btn:hover {
+ transform: scale(1.05);
+}
+
/* Lesson screen */
.lesson {
display: flex;