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>
21 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-komplettes-spielerlebnis | 04 | execute | 3 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 export async function generateImage(prompt: string, referenceImages: Blob[], config: GeminiConfig): Promise; export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise; export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise; export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise;export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise; export function getForestElements(db: IDBDatabase): Promise<ForestElement[]>; export function getStyleReference(db: IDBDatabase): Promise<StyleReference | null>;
export async function getForestComment(characterType: CompanionType, elementDescription: string, db: IDBDatabase): Promise;
export function getRandomFallbackForestComment(): string; import { getRandomFallbackImage } from './fallbacks'; // returns SVG blob
export async function renderForestScene(db: IDBDatabase, newElementId?: number): Promise; export async function initForestScreen(db: IDBDatabase, onStartLesson: (level: number) => void): Promise;
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;
export interface Progress { ... totalCorrect: number; totalErrors: number; errorKeyCounts: Record<string, number>; lastPlayedLevels: number[]; }
Task 1: Reward module with pre-generation and reward screen src/forest/reward.ts, index.html, src/styles/main.css 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 **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);
}
```
**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.
<success_criteria>
- Reward screen shows generated/fallback image with companion comment after every lesson
- Pre-generation starts at lesson begin, reward screen awaits it with placeholder
- Placeholder shows "Der Wald denkt nach..." with animated leaves
- "Weiter ueben" starts next lesson, "Zurueck zum Wald" returns to forest
- New element saved to IndexedDB and animated in forest grid
- Every 3rd session triggers review mode
- After 2x same level, encouragement displayed
- Exercise stats (correct, errors, errorKeys) accumulated in Progress </success_criteria>