diff --git a/index.html b/index.html index b780cd6..90e8bd6 100644 --- a/index.html +++ b/index.html @@ -27,7 +27,15 @@ -
+
+
+

Dein Zauberwald

+
+ +
+ +
+
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;