feat(01-05): forest screen with level map and complete app flow

- Add forest overview screen with 6-level map showing locked/current/completed states
- Create src/forest/scene.ts with getLevelStatus, renderLevelMap, initForestScreen
- Wire complete flow: welcome -> forest -> lesson -> forest with progress updates
- Add forest CSS with level card styles and continue button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 11:15:18 +02:00
co-authored by Claude Opus 4.6
parent d04e09a5a3
commit 5bac8501bf
4 changed files with 225 additions and 37 deletions
+9 -1
View File
@@ -27,7 +27,15 @@
<button class="welcome__start-btn" id="start-btn" disabled>Los geht's!</button>
</div>
</section>
<section id="screen-forest" class="screen"></section>
<section id="screen-forest" class="screen">
<div class="forest">
<h1 class="forest__title">Dein Zauberwald</h1>
<div class="forest__level-map" id="level-map">
<!-- Level cards rendered dynamically -->
</div>
<button class="forest__continue-btn" id="continue-btn">Weiter ueben</button>
</div>
</section>
<section id="screen-lesson" class="screen">
<div class="lesson">
<div class="lesson__letter-area" id="letter-area">
+35 -18
View File
@@ -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,19 +20,7 @@ export function getDB(): IDBDatabase {
return db;
}
export async function startLesson(
level: number,
layout: KeyboardLayout,
): Promise<void> {
// Clean up previous lesson if any
if (lessonCleanup) {
lessonCleanup();
lessonCleanup = null;
}
showScreen("lesson");
lessonCleanup = initLessonScreen(db, level, layout, async (completedLevel: number) => {
async function handleLessonComplete(completedLevel: number): Promise<void> {
// Update progress after lesson completion
const progress = await getProgress(db);
if (!progress) return;
@@ -57,8 +46,30 @@ export async function startLesson(
await saveProgress(db, progress);
// Navigate to forest (per D-07: skip reward in Phase 1)
// 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;
}
// Get layout from progress (already loaded)
getProgress(db).then((progress) => {
const layout = progress?.selectedLayout ?? "de";
showScreen("lesson");
lessonCleanup = initLessonScreen(
db,
level,
layout,
handleLessonComplete,
);
});
}
@@ -66,11 +77,17 @@ export async function initApp(): Promise<void> {
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");
}
}
+66
View File
@@ -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 = `
<span class="level-card__number">Stufe ${level.level}</span>
<span class="level-card__keys">${keysDisplay}${level.newKeys.includes(" ") ? " + Leertaste" : ""}</span>
<span class="level-card__status">${statusIcon}</span>
`;
// 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<void> {
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);
}
+97
View File
@@ -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;