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>
|
<button class="welcome__start-btn" id="start-btn" disabled>Los geht's!</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section id="screen-lesson" class="screen">
|
||||||
<div class="lesson">
|
<div class="lesson">
|
||||||
<div class="lesson__letter-area" id="letter-area">
|
<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 { openDB, getProgress, saveProgress } from "./storage/db";
|
||||||
import { initWelcomeScreen, initLessonScreen } from "./ui/screens";
|
import { initWelcomeScreen, initLessonScreen } from "./ui/screens";
|
||||||
|
import { initForestScreen } from "./forest/scene";
|
||||||
|
|
||||||
let db: IDBDatabase;
|
let db: IDBDatabase;
|
||||||
let lessonCleanup: (() => void) | null = null;
|
let lessonCleanup: (() => void) | null = null;
|
||||||
@@ -19,19 +20,7 @@ export function getDB(): IDBDatabase {
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startLesson(
|
async function handleLessonComplete(completedLevel: number): Promise<void> {
|
||||||
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) => {
|
|
||||||
// Update progress after lesson completion
|
// Update progress after lesson completion
|
||||||
const progress = await getProgress(db);
|
const progress = await getProgress(db);
|
||||||
if (!progress) return;
|
if (!progress) return;
|
||||||
@@ -57,8 +46,30 @@ export async function startLesson(
|
|||||||
|
|
||||||
await saveProgress(db, progress);
|
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");
|
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();
|
db = await openDB();
|
||||||
const progress = await getProgress(db);
|
const progress = await getProgress(db);
|
||||||
if (progress && progress.selectedCharacter) {
|
if (progress && progress.selectedCharacter) {
|
||||||
// Returning user -- go to forest
|
// Returning user -- init forest and go there
|
||||||
|
await initForestScreen(db, startLessonFromForest);
|
||||||
showScreen("forest");
|
showScreen("forest");
|
||||||
} else {
|
} else {
|
||||||
// First time -- show welcome
|
// First time -- show welcome, then forest after setup
|
||||||
initWelcomeScreen(db, showScreen);
|
initWelcomeScreen(db, async (screen: ScreenName) => {
|
||||||
|
showScreen(screen);
|
||||||
|
if (screen === "forest") {
|
||||||
|
await initForestScreen(db, startLessonFromForest);
|
||||||
|
}
|
||||||
|
});
|
||||||
showScreen("welcome");
|
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;
|
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 screen */
|
||||||
.lesson {
|
.lesson {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user