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:
+9
-1
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user