Files
Zauberwald/.planning/phases/03-komplettes-spielerlebnis/03-04-PLAN.md
T
gurixandClaude Opus 4.6 5819ade457 docs(03): create phase plan — komplettes spielerlebnis
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>
2026-03-29 13:37:08 +02:00

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
03-01
03-02
03-03
src/app.ts
src/forest/reward.ts
index.html
src/styles/main.css
true
RWRD-01
RWRD-02
RWRD-03
RWRD-04
TYPE-07
truths artifacts key_links
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
path provides exports
src/forest/reward.ts generateForestReward() pre-generation, initRewardScreen() display
startPreGeneration
initRewardScreen
path provides contains
src/app.ts Updated flow: lesson -> reward -> forest, review/repeat triggers showScreen.*reward
path provides contains
index.html Reward screen HTML structure screen-reward
from to via pattern
src/app.ts src/forest/reward.ts startPreGeneration at lesson start, initRewardScreen at lesson end startPreGeneration
from to via pattern
src/forest/reward.ts src/api/gemini.ts generateImage() for forest element generateImage
from to via pattern
src/forest/reward.ts src/storage/db.ts saveForestElement() to persist saveForestElement
from to via pattern
src/app.ts src/forest/scene.ts renderForestScene(db, newElementId) after reward renderForestScene
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.

<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);
}
```
npx tsc --noEmit && grep -c "startPreGeneration" src/forest/reward.ts && grep -c "initRewardScreen" src/forest/reward.ts && grep -c "reward-continue-btn" index.html - 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 Reward module with pre-generation, placeholder, fallback, IndexedDB persistence. Reward screen HTML with image area, companion comment, two action buttons. All CSS styled. Task 2: Wire complete flow in app.ts with review/repeat triggers src/app.ts 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 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.
npx tsc --noEmit && npx vitest run - 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 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. - `npx tsc --noEmit` — no type errors - `npx vitest run` — all tests pass - Full flow testable: start lesson -> complete 3 phases -> see reward -> navigate back

<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>
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-04-SUMMARY.md`