5 plans across 3 waves covering all 17 requirements: - Plan 01 (W1): Word lists, Progress extension, rate limits - Plan 02 (W2): Lesson 3-phase state machine, review/repeat - Plan 03 (W1): Forest visual scene with SVG + grid - Plan 04 (W3): Reward screen, pre-generation, full flow wiring - Plan 05 (W2): Parent area with stats and settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
516 lines
21 KiB
Markdown
516 lines
21 KiB
Markdown
---
|
|
phase: 03-komplettes-spielerlebnis
|
|
plan: 04
|
|
type: execute
|
|
wave: 3
|
|
depends_on: [03-01, 03-02, 03-03]
|
|
files_modified:
|
|
- src/app.ts
|
|
- src/forest/reward.ts
|
|
- index.html
|
|
- src/styles/main.css
|
|
autonomous: true
|
|
requirements: [RWRD-01, RWRD-02, RWRD-03, RWRD-04, TYPE-07]
|
|
must_haves:
|
|
truths:
|
|
- "After lesson completion, reward screen shows with new forest element image"
|
|
- "Image pre-generation starts at lesson begin and is awaited on reward screen"
|
|
- "Placeholder 'Der Wald denkt nach...' with leaf animation shows while image loads"
|
|
- "Reward screen has 'Weiter ueben' and 'Zurueck zum Wald' buttons"
|
|
- "Full flow works: forest -> lesson (3 phases) -> reward -> forest"
|
|
- "New element persisted in IndexedDB and visible in forest after reward"
|
|
artifacts:
|
|
- path: "src/forest/reward.ts"
|
|
provides: "generateForestReward() pre-generation, initRewardScreen() display"
|
|
exports: ["startPreGeneration", "initRewardScreen"]
|
|
- path: "src/app.ts"
|
|
provides: "Updated flow: lesson -> reward -> forest, review/repeat triggers"
|
|
contains: "showScreen.*reward"
|
|
- path: "index.html"
|
|
provides: "Reward screen HTML structure"
|
|
contains: "screen-reward"
|
|
key_links:
|
|
- from: "src/app.ts"
|
|
to: "src/forest/reward.ts"
|
|
via: "startPreGeneration at lesson start, initRewardScreen at lesson end"
|
|
pattern: "startPreGeneration"
|
|
- from: "src/forest/reward.ts"
|
|
to: "src/api/gemini.ts"
|
|
via: "generateImage() for forest element"
|
|
pattern: "generateImage"
|
|
- from: "src/forest/reward.ts"
|
|
to: "src/storage/db.ts"
|
|
via: "saveForestElement() to persist"
|
|
pattern: "saveForestElement"
|
|
- from: "src/app.ts"
|
|
to: "src/forest/scene.ts"
|
|
via: "renderForestScene(db, newElementId) after reward"
|
|
pattern: "renderForestScene"
|
|
---
|
|
|
|
<objective>
|
|
Wire the complete game flow: lesson -> reward -> forest. Build reward screen with pre-generated forest element image, companion comment, and navigation buttons. Connect review/repetition triggers in app.ts.
|
|
|
|
Purpose: This closes the game loop — the child finishes a lesson, sees a magical reward, and returns to a growing forest.
|
|
Output: Reward module, reward screen HTML/CSS, updated app.ts with full flow including review/repetition logic.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md
|
|
@.planning/phases/03-komplettes-spielerlebnis/03-02-SUMMARY.md
|
|
@.planning/phases/03-komplettes-spielerlebnis/03-03-SUMMARY.md
|
|
@src/app.ts
|
|
@src/api/gemini.ts
|
|
@src/companion/companion.ts
|
|
@src/storage/db.ts
|
|
@src/forest/scene.ts
|
|
@src/types.ts
|
|
@index.html
|
|
@src/companion/fallbacks.ts
|
|
|
|
<interfaces>
|
|
<!-- From src/api/gemini.ts (existing): -->
|
|
export async function generateImage(prompt: string, referenceImages: Blob[], config: GeminiConfig): Promise<Blob | null>;
|
|
export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise<string | null>;
|
|
export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise<boolean>;
|
|
export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise<void>;
|
|
|
|
<!-- From src/storage/db.ts (existing): -->
|
|
export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise<number>;
|
|
export function getForestElements(db: IDBDatabase): Promise<ForestElement[]>;
|
|
export function getStyleReference(db: IDBDatabase): Promise<StyleReference | null>;
|
|
|
|
<!-- From src/companion/companion.ts (existing): -->
|
|
export async function getForestComment(characterType: CompanionType, elementDescription: string, db: IDBDatabase): Promise<string>;
|
|
|
|
<!-- From src/companion/fallbacks.ts (existing): -->
|
|
export function getRandomFallbackForestComment(): string;
|
|
import { getRandomFallbackImage } from './fallbacks'; // returns SVG blob
|
|
|
|
<!-- From src/forest/scene.ts (after Plan 03): -->
|
|
export async function renderForestScene(db: IDBDatabase, newElementId?: number): Promise<void>;
|
|
export async function initForestScreen(db: IDBDatabase, onStartLesson: (level: number) => void): Promise<void>;
|
|
|
|
<!-- From src/ui/screens.ts (after Plan 02): -->
|
|
export function initLessonScreen(
|
|
db: IDBDatabase, level: number, layout: KeyboardLayout,
|
|
onComplete: (level: number, stats: { totalCorrect: number; totalErrors: number; errorKeys: Record<string, number> }) => void,
|
|
onCancel?: () => void,
|
|
isReview?: boolean, showEncouragement?: boolean,
|
|
): () => void;
|
|
|
|
<!-- From src/types.ts (after Plan 01): -->
|
|
export interface Progress {
|
|
...
|
|
totalCorrect: number;
|
|
totalErrors: number;
|
|
errorKeyCounts: Record<string, number>;
|
|
lastPlayedLevels: number[];
|
|
}
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Reward module with pre-generation and reward screen</name>
|
|
<files>src/forest/reward.ts, index.html, src/styles/main.css</files>
|
|
<read_first>src/api/gemini.ts, src/storage/db.ts, src/companion/companion.ts, src/companion/fallbacks.ts, src/types.ts, index.html, src/styles/main.css</read_first>
|
|
<action>
|
|
**Create `src/forest/reward.ts`** (per D-10, D-11, D-12):
|
|
|
|
```typescript
|
|
import { loadConfig } from '../api/config';
|
|
import { generateImage, checkRateLimit, incrementApiCall } from '../api/gemini';
|
|
import { getStyleReference, saveForestElement, getForestElements } from '../storage/db';
|
|
import { getForestComment } from '../companion/companion';
|
|
import { getRandomFallbackForestComment } from '../companion/fallbacks';
|
|
import type { CompanionType, ForestElement } from '../types';
|
|
```
|
|
|
|
**Pre-generation (per D-11, RWRD-02):**
|
|
```typescript
|
|
let pregenPromise: Promise<{ blob: Blob; description: string }> | null = null;
|
|
|
|
export function startPreGeneration(db: IDBDatabase): void {
|
|
pregenPromise = generateForestElement(db);
|
|
}
|
|
```
|
|
|
|
`generateForestElement(db)` async function:
|
|
1. Check rate limit for 'image': `await checkRateLimit(db, 'image')`
|
|
2. If rate-limited, return fallback: use `getRandomFallbackImage()` from fallbacks.ts
|
|
- NOTE: Check if `getRandomFallbackImage` exists. If not, create inline fallback: load one of the SVGs from `src/assets/fallback-images/` via fetch.
|
|
- Actually, look at existing fallbacks.ts — it has `getRandomFallbackImage` or similar. If not, we need a simple function that picks a random fallback SVG and returns it as a Blob.
|
|
3. Load config via `loadConfig()`. If no config, use fallback.
|
|
4. Get style reference via `getStyleReference(db)`. Build referenceImages array (style ref blob if available).
|
|
5. Pick random forest element type from a list: `['Blume', 'Pilz', 'Schmetterling', 'Vogel', 'Reh', 'Eichhoernchen', 'Hase', 'Frosch', 'Igel', 'Marienkaefer', 'Libelle', 'Schnecke']`
|
|
6. Build prompt: `"Kinderbuch-Illustration, Aquarell-Stil, ${elementType}, magischer Wald, warm, einladend, fuer Kinder, Pastellfarben, kein Text, einzelnes Element auf transparentem Hintergrund"`
|
|
7. Call `generateImage(prompt, referenceImages, config)`
|
|
8. If result null, use fallback
|
|
9. Increment API call: `await incrementApiCall(db, 'image')`
|
|
10. Return `{ blob, description: elementType }`
|
|
|
|
**Fallback image function:**
|
|
Add to reward.ts:
|
|
```typescript
|
|
const FALLBACK_SVGS = [
|
|
'/src/assets/fallback-images/blume.svg',
|
|
'/src/assets/fallback-images/pilz.svg',
|
|
'/src/assets/fallback-images/vogel.svg',
|
|
'/src/assets/fallback-images/schmetterling.svg',
|
|
'/src/assets/fallback-images/reh.svg',
|
|
'/src/assets/fallback-images/baum.svg',
|
|
'/src/assets/fallback-images/bach.svg',
|
|
];
|
|
|
|
async function getRandomFallbackImage(): Promise<{ blob: Blob; description: string }> {
|
|
const idx = Math.floor(Math.random() * FALLBACK_SVGS.length);
|
|
const path = FALLBACK_SVGS[idx]!;
|
|
const name = path.split('/').pop()!.replace('.svg', '');
|
|
try {
|
|
const res = await fetch(path);
|
|
const blob = await res.blob();
|
|
return { blob, description: name };
|
|
} catch {
|
|
// Ultimate fallback: empty SVG blob
|
|
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#E8F5E4"/></svg>';
|
|
return { blob: new Blob([svg], { type: 'image/svg+xml' }), description: 'element' };
|
|
}
|
|
}
|
|
```
|
|
|
|
**initRewardScreen (per RWRD-01, RWRD-03, RWRD-04):**
|
|
```typescript
|
|
export async function initRewardScreen(
|
|
db: IDBDatabase,
|
|
level: number,
|
|
characterType: CompanionType,
|
|
onContinue: () => void,
|
|
onBackToForest: () => void,
|
|
): Promise<number> // returns saved element ID
|
|
```
|
|
|
|
1. Show placeholder immediately: set `#reward-image` area to placeholder HTML:
|
|
```html
|
|
<div class="reward__placeholder">
|
|
<span class="reward__leaf reward__leaf--1">🍃</span>
|
|
<span class="reward__leaf reward__leaf--2">🍂</span>
|
|
<span class="reward__leaf reward__leaf--3">🍃</span>
|
|
<p>Der Wald denkt nach...</p>
|
|
</div>
|
|
```
|
|
2. Await `pregenPromise` (or `generateForestElement(db)` if promise is null)
|
|
3. Get companion comment: `await getForestComment(characterType, result.description, db)`
|
|
4. Determine grid position for new element: count existing elements, position = `{ x: col, y: row }` based on next free slot in 4x3 grid (slot index = existingCount % 12). Add random offset +-15px.
|
|
5. Save to IndexedDB:
|
|
```typescript
|
|
const element: ForestElement = {
|
|
levelCompleted: level,
|
|
imageBlob: result.blob,
|
|
imagePrompt: prompt,
|
|
description: result.description,
|
|
companionText: comment,
|
|
position: { x: col, y: row },
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
const elementId = await saveForestElement(db, element);
|
|
```
|
|
6. Replace placeholder with actual image: `URL.createObjectURL(result.blob)` in `<img>` tag
|
|
7. Show companion comment in `#reward-companion-text`
|
|
8. Wire buttons: `#reward-continue-btn` -> onContinue, `#reward-back-btn` -> onBackToForest
|
|
9. Return elementId (for forest scene animation)
|
|
|
|
**Update index.html** — replace empty `<section id="screen-reward">`:
|
|
```html
|
|
<section id="screen-reward" class="screen">
|
|
<div class="reward">
|
|
<h2 class="reward__title">Schau mal!</h2>
|
|
<div class="reward__image-area" id="reward-image-area">
|
|
<!-- Placeholder or generated image -->
|
|
</div>
|
|
<div class="reward__companion" id="reward-companion">
|
|
<img id="reward-companion-avatar" class="companion-area__avatar" src="" alt="">
|
|
<p id="reward-companion-text" class="companion-area__text"></p>
|
|
</div>
|
|
<div class="reward__actions">
|
|
<button class="reward__btn reward__btn--continue" id="reward-continue-btn">Weiter ueben</button>
|
|
<button class="reward__btn reward__btn--back" id="reward-back-btn">Zurueck zum Wald</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
**Add CSS to `src/styles/main.css`:**
|
|
```css
|
|
/* Reward Screen */
|
|
.reward {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 2rem;
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
text-align: center;
|
|
}
|
|
|
|
.reward__title {
|
|
font-family: var(--font-heading);
|
|
color: var(--accent-gold);
|
|
font-size: 1.8rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.reward__image-area {
|
|
width: 280px;
|
|
height: 280px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 1.5rem;
|
|
border-radius: 16px;
|
|
background: var(--bg-forest);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.reward__image-area img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
animation: reward-image-appear 0.8s ease-out;
|
|
}
|
|
|
|
@keyframes reward-image-appear {
|
|
from { opacity: 0; transform: scale(0.5); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
.reward__placeholder {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
color: var(--text-warm);
|
|
font-family: var(--font-heading);
|
|
}
|
|
|
|
.reward__leaf {
|
|
font-size: 1.5rem;
|
|
animation: leaf-float 2s ease-in-out infinite;
|
|
}
|
|
|
|
.reward__leaf--1 { animation-delay: 0s; }
|
|
.reward__leaf--2 { animation-delay: 0.5s; }
|
|
.reward__leaf--3 { animation-delay: 1s; }
|
|
|
|
@keyframes leaf-float {
|
|
0%, 100% { transform: translateY(0) rotate(0deg); }
|
|
50% { transform: translateY(-10px) rotate(15deg); }
|
|
}
|
|
|
|
.reward__companion {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
padding: 1rem;
|
|
background: rgba(255, 255, 255, 0.7);
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.reward__actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.reward__btn {
|
|
font-family: var(--font-heading);
|
|
font-size: 1.1rem;
|
|
padding: 0.75rem 1.5rem;
|
|
border: none;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.reward__btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.reward__btn--continue {
|
|
background: var(--accent-green);
|
|
color: white;
|
|
}
|
|
|
|
.reward__btn--back {
|
|
background: var(--bg-cream);
|
|
color: var(--text-warm);
|
|
border: 2px solid var(--accent-green);
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>npx tsc --noEmit && grep -c "startPreGeneration" src/forest/reward.ts && grep -c "initRewardScreen" src/forest/reward.ts && grep -c "reward-continue-btn" index.html</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep "startPreGeneration" src/forest/reward.ts returns export
|
|
- grep "initRewardScreen" src/forest/reward.ts returns export
|
|
- grep "generateImage" src/forest/reward.ts shows image generation call
|
|
- grep "saveForestElement" src/forest/reward.ts shows persistence
|
|
- grep "Der Wald denkt nach" src/forest/reward.ts shows placeholder text
|
|
- grep "reward-continue-btn" index.html returns match
|
|
- grep "reward-back-btn" index.html returns match
|
|
- grep "reward__placeholder" src/styles/main.css returns match
|
|
- grep "leaf-float" src/styles/main.css returns match (animated leaves)
|
|
- npx tsc --noEmit exits 0
|
|
</acceptance_criteria>
|
|
<done>Reward module with pre-generation, placeholder, fallback, IndexedDB persistence. Reward screen HTML with image area, companion comment, two action buttons. All CSS styled.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire complete flow in app.ts with review/repeat triggers</name>
|
|
<files>src/app.ts</files>
|
|
<read_first>src/app.ts, src/forest/reward.ts, src/forest/scene.ts, src/ui/screens.ts, src/types.ts, src/storage/db.ts, src/companion/characters.ts</read_first>
|
|
<action>
|
|
Rewrite flow logic in `src/app.ts` (per D-10, D-11, D-05, D-06, TYPE-07):
|
|
|
|
**Import additions:**
|
|
```typescript
|
|
import { startPreGeneration, initRewardScreen } from './forest/reward';
|
|
import { renderForestScene } from './forest/scene';
|
|
```
|
|
|
|
**Update `startLessonFromForest(level: number)`:**
|
|
1. Determine if this is a review session (per D-05): `progress.totalSessions > 0 && progress.totalSessions % 3 === 0`
|
|
2. Determine if encouragement needed (per D-06): check `progress.lastPlayedLevels` — if last 2 entries are the same level as current, set `showEncouragement = true`
|
|
3. Start pre-generation: `startPreGeneration(db)` — fire and forget (per D-11)
|
|
4. Call updated `initLessonScreen(db, level, layout, handleLessonComplete, handleLessonCancel, isReview, showEncouragement)`
|
|
|
|
**Update `handleLessonComplete`:**
|
|
Change signature to accept stats:
|
|
```typescript
|
|
async function handleLessonComplete(
|
|
completedLevel: number,
|
|
stats: { totalCorrect: number; totalErrors: number; errorKeys: Record<string, number> }
|
|
): Promise<void>
|
|
```
|
|
|
|
1. Update progress with stats (per D-15, D-16):
|
|
```typescript
|
|
progress.totalCorrect += stats.totalCorrect;
|
|
progress.totalErrors += stats.totalErrors;
|
|
for (const [key, count] of Object.entries(stats.errorKeys)) {
|
|
progress.errorKeyCounts[key] = (progress.errorKeyCounts[key] ?? 0) + count;
|
|
}
|
|
// Track last played levels (keep last 3)
|
|
progress.lastPlayedLevels.push(completedLevel);
|
|
if (progress.lastPlayedLevels.length > 3) {
|
|
progress.lastPlayedLevels = progress.lastPlayedLevels.slice(-3);
|
|
}
|
|
```
|
|
2. Existing level advancement logic (keep as-is)
|
|
3. Save progress
|
|
4. Show REWARD screen instead of going directly to forest:
|
|
```typescript
|
|
showScreen('reward');
|
|
const elementId = await initRewardScreen(
|
|
db,
|
|
completedLevel,
|
|
progress.selectedCharacter,
|
|
() => {
|
|
// "Weiter ueben" — start next lesson
|
|
startLessonFromForest(Math.min(progress.currentLevel, MAX_LEVEL));
|
|
},
|
|
async () => {
|
|
// "Zurueck zum Wald" — show forest with new element animated
|
|
await initForestScreen(db, startLessonFromForest);
|
|
await renderForestScene(db, elementId);
|
|
showScreen('forest');
|
|
},
|
|
);
|
|
```
|
|
|
|
**Set companion avatar on reward screen:**
|
|
After `showScreen('reward')`, set the reward companion avatar:
|
|
```typescript
|
|
const companion = companions.find(c => c.type === progress.selectedCharacter);
|
|
if (companion) {
|
|
const avatar = document.getElementById('reward-companion-avatar') as HTMLImageElement;
|
|
avatar.src = companion.avatarUrl;
|
|
avatar.alt = companion.name;
|
|
}
|
|
```
|
|
|
|
**Handle backward compatibility:**
|
|
The `handleLessonComplete` in `startLessonFromForest` is passed as callback. Make sure the `initLessonScreen` onComplete signature matches (it was updated in Plan 02 to pass stats).
|
|
|
|
**Handle missing fields in existing progress data:**
|
|
When loading progress, ensure new fields have defaults:
|
|
```typescript
|
|
if (!progress.errorKeyCounts) progress.errorKeyCounts = {};
|
|
if (!progress.lastPlayedLevels) progress.lastPlayedLevels = [];
|
|
if (progress.totalCorrect === undefined) progress.totalCorrect = 0;
|
|
if (progress.totalErrors === undefined) progress.totalErrors = 0;
|
|
```
|
|
Add this migration logic after `getProgress(db)` calls.
|
|
|
|
**Parent area shortcut:**
|
|
Add global keydown listener for Ctrl+Shift+E (this will be used by Plan 05, but wire the detection here):
|
|
```typescript
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.ctrlKey && e.shiftKey && e.key === 'E') {
|
|
e.preventDefault();
|
|
showParentCodePrompt();
|
|
}
|
|
});
|
|
```
|
|
For now, `showParentCodePrompt` can be a stub that will be implemented in Plan 05.
|
|
</action>
|
|
<verify>
|
|
<automated>npx tsc --noEmit && npx vitest run</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep "startPreGeneration" src/app.ts returns match (pre-gen at lesson start)
|
|
- grep "initRewardScreen" src/app.ts returns match (reward after lesson)
|
|
- grep "showScreen.*reward" src/app.ts returns match (navigate to reward)
|
|
- grep "renderForestScene" src/app.ts returns match (forest update after reward)
|
|
- grep "totalSessions.*% 3" src/app.ts returns match (review trigger)
|
|
- grep "lastPlayedLevels" src/app.ts returns match (repeat detection)
|
|
- grep "errorKeyCounts" src/app.ts returns match (stats accumulation)
|
|
- grep "Ctrl.*Shift.*E" src/app.ts or grep "ctrlKey.*shiftKey" src/app.ts returns match
|
|
- npx tsc --noEmit exits 0
|
|
</acceptance_criteria>
|
|
<done>Complete flow wired: forest -> lesson (with pre-gen) -> reward (with image + comment) -> forest (with animation). Review triggers every 3rd session. Repetition protection tracks last played levels. Stats accumulated to progress.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `npx tsc --noEmit` — no type errors
|
|
- `npx vitest run` — all tests pass
|
|
- Full flow testable: start lesson -> complete 3 phases -> see reward -> navigate back
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
1. Reward screen shows generated/fallback image with companion comment after every lesson
|
|
2. Pre-generation starts at lesson begin, reward screen awaits it with placeholder
|
|
3. Placeholder shows "Der Wald denkt nach..." with animated leaves
|
|
4. "Weiter ueben" starts next lesson, "Zurueck zum Wald" returns to forest
|
|
5. New element saved to IndexedDB and animated in forest grid
|
|
6. Every 3rd session triggers review mode
|
|
7. After 2x same level, encouragement displayed
|
|
8. Exercise stats (correct, errors, errorKeys) accumulated in Progress
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-04-SUMMARY.md`
|
|
</output>
|