diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 86514c2..dfc282a 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -68,7 +68,15 @@ Plans: 3. Alle Stufen 1–6 sind spielbar, jede mit passenden Woertern aus gelernten Buchstaben 4. Der Elternbereich ist erreichbar (Ctrl+Shift+E + "1234") und zeigt aktuelle Stufe, Einheiten, Uebungstage sowie Einstellungen 5. Review-Einheiten erscheinen jede 3. Uebung; nach 2× derselben Stufe zeigt die Begleitfigur Ermutigung -**Plans**: TBD +**Plans:** 5 plans + +Plans: +- [ ] 03-01-PLAN.md — Word lists per level, Progress type extension, rate limit update +- [ ] 03-02-PLAN.md — Lesson 3-phase state machine (discover/practice/words), review + repetition logic +- [ ] 03-03-PLAN.md — Forest visual scene (SVG background, 4x3 grid, element rendering from IndexedDB) +- [ ] 03-04-PLAN.md — Reward screen with pre-generation, full flow wiring (lesson -> reward -> forest) +- [ ] 03-05-PLAN.md — Parent area (access gate, stats overview, settings panel) + **UI hint**: yes ### Phase 4: Polish + Audio @@ -93,5 +101,5 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 |-------|----------------|--------|-----------| | 1. Grundgerüst + Tippmechanik | 0/5 | Planning complete | - | | 2. Gemini-Integration + Asset-Pipeline | 0/5 | Planning complete | - | -| 3. Komplettes Spielerlebnis | 0/? | Not started | - | +| 3. Komplettes Spielerlebnis | 0/5 | Planning complete | - | | 4. Polish + Audio | 0/? | Not started | - | diff --git a/.planning/phases/03-komplettes-spielerlebnis/03-01-PLAN.md b/.planning/phases/03-komplettes-spielerlebnis/03-01-PLAN.md new file mode 100644 index 0000000..242d587 --- /dev/null +++ b/.planning/phases/03-komplettes-spielerlebnis/03-01-PLAN.md @@ -0,0 +1,177 @@ +--- +phase: 03-komplettes-spielerlebnis +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/types.ts + - src/game/words.ts + - src/game/words.test.ts + - src/api/gemini.ts +autonomous: true +requirements: [LEVL-04, TYPE-06] +must_haves: + truths: + - "Word lists exist for levels 1-6 using only learned letters" + - "Every word in a level's list uses only letters from allKeys of that level" + - "Progress interface has stats fields for accuracy tracking" + - "Rate limits allow 10 text + 5 image calls per day" + artifacts: + - path: "src/game/words.ts" + provides: "Static word lists per level, getWordsForLevel() function" + exports: ["wordsByLevel", "getWordsForLevel"] + - path: "src/game/words.test.ts" + provides: "Tests validating word lists only use learned letters" + - path: "src/types.ts" + provides: "Extended Progress with totalCorrect, totalErrors, errorKeyCounts, lastPlayedLevels" + contains: "totalCorrect" + - path: "src/api/gemini.ts" + provides: "Updated rate limits: 10 text, 5 image per day" + key_links: + - from: "src/game/words.ts" + to: "src/game/levels.ts" + via: "imports getKeysUpToLevel for validation" + pattern: "getKeysUpToLevel" +--- + + +Foundation data layer for Phase 3: word lists per level, extended Progress type with stats fields, updated rate limits. + +Purpose: All subsequent plans (lesson flow, reward, parent area) depend on these types and data structures. +Output: `src/game/words.ts` with tested word lists, extended `Progress` interface, updated rate limits in gemini.ts. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@src/types.ts +@src/game/levels.ts +@src/api/gemini.ts + + + + + + Task 1: Word lists per level with validation tests + src/game/words.ts, src/game/words.test.ts + src/game/levels.ts, src/types.ts + + - Test: wordsByLevel has entries for levels 1-6 + - Test: Each level has at least 8 words (per D-04: 8-10 minimum) + - Test: Every word in level N uses only characters from levels[N].allKeys (no spaces) + - Test: No word exceeds 5 characters (per D-04) + - Test: getWordsForLevel(level, count) returns `count` random words from that level + - Test: getWordsForLevel returns empty array for non-existent level + + + Create `src/game/words.ts` (per D-04): + + ```typescript + import { getKeysUpToLevel } from './levels'; + + export const wordsByLevel: Record = { + 1: [], // Level 1 has only F, J, space — no real words possible, use letter combos: "ff", "jj", "fj", "jf", "fjf", "jfj", "ff", "jj", "fjfj", "jfjf" + 2: ['fdk', 'kdf', 'djf', 'fkd', 'dkf', 'jdk', 'kfj', 'fdkj', 'kjdf', 'dfjk'], // F,J,D,K — no real German words, use combos + 3: ['falls', 'lass', 'das', 'sdf', 'sdk', 'lsd', 'flsk', 'sldf', 'dsl', 'kls'], // adds S, L — "falls", "lass" are real words + 4: ['als', 'da', 'das', 'dafö', 'salad', 'lass', 'falls', 'aas', 'sa', 'ask'], // adds A, OE + 5: ['glas', 'halb', 'jagd', 'gash', 'hagl', 'gah', 'hag', 'lag', 'gal', 'sah'], // adds G, H + 6: ['eis', 'idee', 'kleid', 'lied', 'lief', 'fies', 'dies', 'die', 'sie', 'sei'], // adds E, I + }; + ``` + + IMPORTANT: For levels 1-2, real German words are impossible with only F/J/D/K + space. Use letter combination patterns instead (the Spec acknowledges this implicitly — words only become meaningful at level 3+). Each word MUST only use characters from that level's `allKeys` (excluding space). Validate at test time by cross-referencing `getKeysUpToLevel()`. + + Export `getWordsForLevel(level: number, count: number): string[]` that returns `count` random words from the level's list (sampling without replacement if count <= list length, with replacement otherwise). + + Create `src/game/words.test.ts` with vitest tests covering all behaviors above. + + + npx vitest run src/game/words.test.ts + + + - grep "wordsByLevel" src/game/words.ts returns matches + - grep "getWordsForLevel" src/game/words.ts returns export + - grep "getKeysUpToLevel" src/game/words.test.ts shows validation against level keys + - npx vitest run src/game/words.test.ts exits 0 + + Word lists for levels 1-6 exist, each with 8+ words using only learned letters, max 5 chars. Tests pass. + + + + Task 2: Extend Progress type and update rate limits + src/types.ts, src/api/gemini.ts + src/types.ts, src/api/gemini.ts + + **Progress type extension (per D-15):** + Add these fields to the `Progress` interface in `src/types.ts`: + ```typescript + totalCorrect: number; + totalErrors: number; + errorKeyCounts: Record; + lastPlayedLevels: number[]; // last 3 played levels for D-06 + ``` + + Also add a `LessonPhase` type: + ```typescript + export type LessonPhase = 'discover' | 'practice' | 'words'; + ``` + + **Update rate limits (per D-13):** + In `src/api/gemini.ts`, change `checkRateLimit` function: + - Current: `settings.apiCallsToday[type] < 1` (allows 1 each) + - New: text limit = 10, image limit = 5 + - Add constants: `const TEXT_RATE_LIMIT = 10;` and `const IMAGE_RATE_LIMIT = 5;` + - Change comparison: `type === 'text' ? settings.apiCallsToday.text < TEXT_RATE_LIMIT : settings.apiCallsToday.image < IMAGE_RATE_LIMIT` + + **Update initWelcomeScreen in screens.ts** to include new Progress fields with defaults: + In `src/ui/screens.ts`, find the `Progress` object creation in `startBtn` click handler and add: + ```typescript + totalCorrect: 0, + totalErrors: 0, + errorKeyCounts: {}, + lastPlayedLevels: [], + ``` + + + npx vitest run && npx tsc --noEmit + + + - grep "totalCorrect" src/types.ts returns a match + - grep "totalErrors" src/types.ts returns a match + - grep "errorKeyCounts" src/types.ts returns a match + - grep "lastPlayedLevels" src/types.ts returns a match + - grep "LessonPhase" src/types.ts returns a match + - grep "TEXT_RATE_LIMIT = 10" src/api/gemini.ts returns a match + - grep "IMAGE_RATE_LIMIT = 5" src/api/gemini.ts returns a match + - npx tsc --noEmit exits 0 + + Progress has stats fields, LessonPhase type exists, rate limits updated to 10 text / 5 image per day. TypeScript compiles clean. + + + + + +- `npx vitest run` — all tests pass +- `npx tsc --noEmit` — no type errors +- Word lists validated: every word uses only learned letters for its level + + + +1. `src/game/words.ts` exports wordsByLevel (levels 1-6, 8+ words each) and getWordsForLevel() +2. All words validated: only use allKeys from their level, max 5 chars +3. Progress interface extended with totalCorrect, totalErrors, errorKeyCounts, lastPlayedLevels +4. LessonPhase type exported from types.ts +5. Rate limits updated to 10 text / 5 image per day +6. All tests pass, TypeScript compiles clean + + + +After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md` + diff --git a/.planning/phases/03-komplettes-spielerlebnis/03-02-PLAN.md b/.planning/phases/03-komplettes-spielerlebnis/03-02-PLAN.md new file mode 100644 index 0000000..f0d4bf9 --- /dev/null +++ b/.planning/phases/03-komplettes-spielerlebnis/03-02-PLAN.md @@ -0,0 +1,340 @@ +--- +phase: 03-komplettes-spielerlebnis +plan: 02 +type: execute +wave: 2 +depends_on: [03-01] +files_modified: + - src/game/typing.ts + - src/game/typing.test.ts + - src/ui/screens.ts + - index.html +autonomous: true +requirements: [TYPE-05, TYPE-06, TYPE-07, LEVL-05, LEVL-06] +must_haves: + truths: + - "Entdecken phase shows companion text and highlights the new key on keyboard" + - "Woerter phase displays whole words with active letter highlighted" + - "Lesson flows through discover -> practice -> words automatically" + - "Every 3rd session is a review mixing letters from last 2-3 levels" + - "After 2x same level, companion shows encouragement with curiosity trigger" + artifacts: + - path: "src/game/typing.ts" + provides: "Extended createExerciseState with mode='letters'|'words', generateWordLetters()" + exports: ["createExerciseState", "generateWordLetters"] + - path: "src/ui/screens.ts" + provides: "Lesson sub-phase state machine: discover -> practice -> words" + contains: "LessonPhase" + key_links: + - from: "src/ui/screens.ts" + to: "src/game/typing.ts" + via: "createExerciseState for practice and word phases" + pattern: "createExerciseState" + - from: "src/ui/screens.ts" + to: "src/companion/companion.ts" + via: "getLetterIntro for discover phase" + pattern: "getLetterIntro" + - from: "src/ui/screens.ts" + to: "src/game/words.ts" + via: "getWordsForLevel for word phase" + pattern: "getWordsForLevel" +--- + + +Build the complete lesson flow with 3 sub-phases (Entdecken -> Ueben -> Woerter) as an internal state machine, plus review mechanic and repetition protection. + +Purpose: This is the core game loop — the child experiences discover, practice, then words in one fluid session. +Output: Extended typing engine with word mode, lesson screen with 3-phase state machine, review and repetition logic. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md +@src/game/typing.ts +@src/ui/screens.ts +@src/companion/companion.ts +@src/game/words.ts +@src/game/levels.ts +@src/types.ts +@index.html + + + +export type LessonPhase = 'discover' | 'practice' | 'words'; +export interface Progress { + id: 1; + currentLevel: number; + completedLevels: number[]; + totalSessions: number; + sessionDates: string[]; + selectedCharacter: CompanionType; + selectedLayout: KeyboardLayout; + totalCorrect: number; + totalErrors: number; + errorKeyCounts: Record; + lastPlayedLevels: number[]; +} + + +export const wordsByLevel: Record; +export function getWordsForLevel(level: number, count: number): string[]; + + +export async function getLetterIntro(characterType: CompanionType, letter: string, fingerDescription: string, db: IDBDatabase): Promise; + + +export function createExerciseState(level: number): TypingExerciseState; +export function handleKeyPress(state: TypingExerciseState, pressedKey: string): KeyPressResult; +export function getCurrentLetter(state: TypingExerciseState): string | null; + + +export function getLevelByNumber(n: number): Level | undefined; +export function getKeysUpToLevel(n: number): string[]; + + + + + + + Task 1: Extend typing engine with word mode + src/game/typing.ts, src/game/typing.test.ts + src/game/typing.ts, src/game/words.ts, src/types.ts + + - Test: createExerciseState(level) still works as before (letters mode, backwards compatible) + - Test: createWordExerciseState(level, words) creates state with letters from the words joined, e.g. words=["eis","die"] -> letters=["e","i","s","d","i","e"] + - Test: createWordExerciseState stores words and wordBoundaries in state for UI rendering + - Test: handleKeyPress works the same way for word-mode state + - Test: getWordAtIndex(state, letterIndex) returns which word the letter belongs to + + + Extend `src/game/typing.ts` (per D-03): + + 1. Add `ExerciseMode` type: `'letters' | 'words'` + + 2. Extend `TypingExerciseState` interface (in types.ts) with optional word fields: + ```typescript + export interface TypingExerciseState { + currentLetterIndex: number; + letters: string[]; + intervalMs: number; + correctStreak: number; + totalCorrect: number; + totalErrors: number; + mode: 'letters' | 'words'; + words?: string[]; // original words for display + wordBoundaries?: number[]; // indices where each word starts in letters[] + } + ``` + + 3. Keep existing `createExerciseState(level)` unchanged but set `mode: 'letters'`, `words: undefined`, `wordBoundaries: undefined`. + + 4. Add new function: + ```typescript + export function createWordExerciseState(words: string[]): TypingExerciseState { + const letters: string[] = []; + const wordBoundaries: number[] = []; + for (const word of words) { + wordBoundaries.push(letters.length); + for (const ch of word) { + letters.push(ch); + } + } + return { + currentLetterIndex: 0, + letters, + intervalMs: 0, // no falling in word mode + correctStreak: 0, + totalCorrect: 0, + totalErrors: 0, + mode: 'words', + words, + wordBoundaries, + }; + } + ``` + + 5. Add helper: `getCurrentWordIndex(state: TypingExerciseState): number` — returns which word index (0-based) the currentLetterIndex is in, using wordBoundaries. + + 6. `handleKeyPress` already works generically — no changes needed. + + Create `src/game/typing.test.ts` (or extend if exists) with vitest tests for the new functions plus backwards compatibility. + + + npx vitest run src/game/typing.test.ts + + + - grep "createWordExerciseState" src/game/typing.ts returns export + - grep "getCurrentWordIndex" src/game/typing.ts returns export + - grep "mode.*letters.*words" src/types.ts returns match showing mode field + - npx vitest run src/game/typing.test.ts exits 0 + + Typing engine supports word mode with word boundaries tracking. Backwards compatible with letter mode. Tests pass. + + + + Task 2: Lesson screen 3-phase state machine with review and repetition + src/ui/screens.ts, index.html + src/ui/screens.ts, src/game/typing.ts, src/companion/companion.ts, src/game/words.ts, src/game/levels.ts, src/types.ts, index.html + + Rewrite `initLessonScreen` in `src/ui/screens.ts` as a 3-phase state machine (per D-01, D-02, D-03): + + **Add to index.html** inside `#screen-lesson .lesson`: + ```html +
+ ``` + This shows which phase is active (Entdecken / Ueben / Woerter). Add above the letter-area. + + **Signature change:** + ```typescript + export function initLessonScreen( + db: IDBDatabase, + level: number, + layout: KeyboardLayout, + onComplete: (level: number, stats: { totalCorrect: number; totalErrors: number; errorKeys: Record }) => void, + onCancel?: () => void, + ): () => void + ``` + The `onComplete` callback now receives exercise stats (per D-16). + + **State machine flow:** + + 1. **DISCOVER phase** (per D-02, TYPE-05): + - Show phase indicator: "Entdecken" + - Get the first new key of this level via `getLevelByNumber(level).newKeys[0]` + - Fetch companion letter intro via `getLetterIntro(characterType, letter, fingerDescription, db)` + - Display intro text in `#lesson-companion-text` + - Highlight the target key on keyboard (already have `highlightKey()`) + - Make key pulse (add class `keyboard__key--discover-pulse` to the key) + - Wait for child to press the correct key ONCE to proceed + - If level has a second newKey, repeat discover for that key too + - Then transition to PRACTICE + + 2. **PRACTICE phase** (existing behavior, TYPE-01-04): + - Show phase indicator: "Ueben" + - Run existing letter exercise with `createExerciseState(level)` + - On complete, accumulate stats, transition to WORDS + + 3. **WORDS phase** (per D-03, TYPE-06): + - Show phase indicator: "Woerter" + - Get 3-4 random words via `getWordsForLevel(level, 4)` (fallback to 3 if < 4 available) + - Create word exercise via `createWordExerciseState(words)` + - Display current word fully, highlight active letter: + ```html +
+ e + i + s +
+ ``` + - When word complete, show next word. After all words done, call `onComplete(level, stats)`. + + **Review mechanic (per D-05, LEVL-05):** + Add `isReviewSession` parameter to `initLessonScreen`. When `true`: + - Discover phase: skip (reviews don't introduce new letters) + - Practice phase: use mixed letters from last 2-3 levels (combine allKeys from current and previous 1-2 levels) + - Words phase: pick words from last 2-3 levels randomly + + The review trigger logic (`totalSessions % 3 === 0`) is handled in `app.ts` (Plan 04), not here. This function just needs to accept the `isReview: boolean` flag. + + **Repetition protection (per D-06, LEVL-06):** + Add `showEncouragement` parameter. When `true`: + - Before discover phase, show companion encouragement text: a hardcoded array of 3-4 messages like "Magst du sehen, was als Naechstes kommt? Im Wald gibt es noch so viel zu entdecken!" + - Show for 3 seconds, then proceed to discover (or practice if review). + + The trigger logic (checking `lastPlayedLevels` for 2x same) is handled in `app.ts` (Plan 04). + + **Cumulative stats across phases:** + Track `totalCorrect`, `totalErrors`, `errorKeys: Record` across all 3 phases. In handleKeyPress wrapper, if `!result.correct`, increment `errorKeys[expectedKey]`. Pass accumulated stats to `onComplete`. + + **CSS for phase indicator** — add styles in action: + ```css + .lesson__phase-indicator { + text-align: center; + font-family: var(--font-heading); + font-size: 1rem; + color: var(--text-warm); + margin-bottom: 0.5rem; + opacity: 0.7; + } + ``` + + **CSS for word display:** + ```css + .word-display { + display: flex; + justify-content: center; + gap: 0.25rem; + font-family: var(--font-heading); + font-size: 2.5rem; + margin: 2rem 0; + } + .word-display__letter { + color: var(--text-light); + transition: color 0.3s; + } + .word-display__letter--active { + color: var(--accent-gold); + font-weight: 700; + } + .word-display__letter--done { + color: var(--accent-green); + } + ``` + + Add CSS for discover pulse: + ```css + .keyboard__key--discover-pulse { + animation: discover-pulse 1.5s ease-in-out infinite; + } + @keyframes discover-pulse { + 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 var(--accent-gold); } + 50% { transform: scale(1.1); box-shadow: 0 0 12px 4px var(--accent-gold); } + } + ``` +
+ + npx tsc --noEmit && npx vitest run + + + - grep "lesson-phase-indicator" index.html returns a match + - grep "discover.*practice.*words" src/ui/screens.ts shows state machine phases + - grep "getLetterIntro" src/ui/screens.ts shows companion integration in discover + - grep "createWordExerciseState" src/ui/screens.ts shows word mode usage + - grep "getWordsForLevel" src/ui/screens.ts shows word list integration + - grep "isReview" src/ui/screens.ts shows review parameter + - grep "showEncouragement" src/ui/screens.ts shows repetition protection parameter + - grep "word-display" src/styles/main.css returns CSS matches + - grep "discover-pulse" src/styles/main.css returns CSS matches + - npx tsc --noEmit exits 0 + + Lesson screen runs 3 phases (discover->practice->words) with companion letter intro, word display UI, review flag support, and repetition encouragement. Stats accumulated across phases and passed to onComplete callback. +
+ +
+ + +- `npx tsc --noEmit` — no type errors +- `npx vitest run` — all tests pass +- Manual: lesson screen shows phase indicator, discover phase displays companion text and highlights key, words phase shows word with active letter + + + +1. Typing engine has createWordExerciseState() and getCurrentWordIndex() +2. Lesson screen flows: discover -> practice -> words -> onComplete(level, stats) +3. Discover phase: companion introduces letter, key pulses, child presses once to proceed +4. Words phase: 3-4 words displayed with active letter highlighted, typed letter by letter +5. Review flag skips discover and mixes letters/words from recent levels +6. Encouragement shown when showEncouragement=true +7. Stats (correct, errors, errorKeys) accumulated and passed to onComplete + + + +After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-02-SUMMARY.md` + diff --git a/.planning/phases/03-komplettes-spielerlebnis/03-03-PLAN.md b/.planning/phases/03-komplettes-spielerlebnis/03-03-PLAN.md new file mode 100644 index 0000000..2ce4d14 --- /dev/null +++ b/.planning/phases/03-komplettes-spielerlebnis/03-03-PLAN.md @@ -0,0 +1,267 @@ +--- +phase: 03-komplettes-spielerlebnis +plan: 03 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/forest/scene.ts + - index.html + - src/styles/main.css +autonomous: true +requirements: [FRST-01, FRST-02, FRST-03] +must_haves: + truths: + - "Forest screen shows SVG woodland background" + - "A 4x3 grid overlays the SVG for placing forest element images" + - "New elements appear with fade-in + scale-up animation" + - "All previously earned elements load from IndexedDB on screen init" + artifacts: + - path: "src/forest/scene.ts" + provides: "renderForestScene() that loads elements from IDB and renders in grid" + exports: ["renderForestScene"] + - path: "index.html" + provides: "Forest scene container with SVG background and grid overlay" + contains: "forest-scene" + key_links: + - from: "src/forest/scene.ts" + to: "src/storage/db.ts" + via: "getForestElements() to load persisted elements" + pattern: "getForestElements" +--- + + +Build the visual forest scene: SVG background with a 4x3 grid overlay where generated forest element images are placed. Elements load from IndexedDB and new ones animate in. + +Purpose: The growing forest IS the reward system — the child sees their world fill up with each session. +Output: Visual forest scene in forest screen with grid, animations, and IndexedDB loading. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@src/forest/scene.ts +@src/storage/db.ts +@src/types.ts +@index.html +@src/styles/main.css + + + +export function getForestElements(db: IDBDatabase): Promise; +export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise; + + +export interface ForestElement { + id?: number; + levelCompleted: number; + imageBlob: Blob; + imagePrompt: string; + description: string; + companionText: string; + position: { x: number; y: number }; + createdAt: string; +} + + + + + + + Task 1: SVG forest background and grid HTML structure + index.html, src/styles/main.css + index.html, src/styles/main.css, src/forest/scene.ts + + **Update index.html** `#screen-forest .forest` section (per D-07, D-09): + + Add a forest scene container ABOVE the level-map but BELOW the greeting: + ```html +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ ``` + + NOTE: The SVG layers must be ordered so sky-rect is FIRST (painted first, at back), then hills/ground on top. Adjust SVG element order: sky rect first, then clouds/sun, then background trees, then hills, then ground. + + **Add CSS to `src/styles/main.css`:** + + ```css + /* Forest Scene */ + .forest__scene { + position: relative; + width: 100%; + max-width: 800px; + margin: 0 auto 1.5rem; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + } + + .forest__bg { + display: block; + width: 100%; + height: auto; + } + + .forest__grid { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(3, 1fr); + padding: 1rem; + gap: 0.5rem; + } + + .forest__element { + display: flex; + align-items: center; + justify-content: center; + } + + .forest__element img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 8px; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15)); + } + + /* New element animation (per D-08, FRST-02) */ + .forest__element--new { + animation: forest-element-appear 1.2s ease-out forwards; + } + + @keyframes forest-element-appear { + 0% { + opacity: 0; + transform: scale(0.3); + } + 60% { + opacity: 1; + transform: scale(1.05); + } + 100% { + opacity: 1; + transform: scale(1); + } + } + ``` +
+ + grep -c "forest-scene" index.html && grep -c "forest__grid" index.html && grep -c "forest__element--new" src/styles/main.css + + + - grep "forest-scene" index.html returns match + - grep "forest__grid" index.html returns match + - grep "forest-element-appear" src/styles/main.css returns match (animation keyframes) + - grep "grid-template-columns.*repeat(4" src/styles/main.css returns match (4x3 grid) + - grep "grid-template-rows.*repeat(3" src/styles/main.css returns match + - SVG viewBox="0 0 800 400" present in index.html + + Forest scene has SVG background with woodland scenery, 4x3 CSS grid overlay, and fade-in+scale-up animation keyframes for new elements. +
+ + + Task 2: Render forest elements from IndexedDB + src/forest/scene.ts + src/forest/scene.ts, src/storage/db.ts, src/types.ts, index.html + + Extend `src/forest/scene.ts` (per D-07, D-08, FRST-03): + + Add a new exported function `renderForestScene(db: IDBDatabase, newElementId?: number): Promise`: + + 1. Get `#forest-grid` element + 2. Call `getForestElements(db)` to load all persisted elements + 3. Clear grid contents + 4. For each element (up to 12 — the grid has 12 slots): + - Create a `
` + - Create an `` with `src` = `URL.createObjectURL(element.imageBlob)` + - Set `alt` = `element.description` + - Apply random offset per D-07: `style="transform: translate(${randomOffset}px, ${randomOffset}px)"` where offset is +-10-20px random + - If `element.id === newElementId`, add class `forest__element--new` for animation + - Append to grid + + 5. Store created Object URLs in a module-level array so they can be revoked on next call (prevent memory leak): + ```typescript + let activeObjectUrls: string[] = []; + ``` + At start of function, revoke all old URLs via `URL.revokeObjectURL()`, then clear array. + + Update `initForestScreen` to call `renderForestScene(db)` alongside existing `renderLevelMap`. + + The `newElementId` parameter will be used by Plan 04 (reward flow) to trigger animation on the just-added element. + + + npx tsc --noEmit && npx vitest run + + + - grep "renderForestScene" src/forest/scene.ts returns export + - grep "getForestElements" src/forest/scene.ts returns match (loads from IDB) + - grep "createObjectURL" src/forest/scene.ts returns match (blob->URL conversion) + - grep "revokeObjectURL" src/forest/scene.ts returns match (memory cleanup) + - grep "forest__element--new" src/forest/scene.ts returns match (animation class) + - grep "renderForestScene" src/forest/scene.ts shows it's called from initForestScreen + - npx tsc --noEmit exits 0 + + Forest scene renders all stored elements from IndexedDB as images in the 4x3 grid. New elements get animation class. Object URLs cleaned up on re-render. Level map preserved alongside scene. + + + + + +- `npx tsc --noEmit` — no type errors +- `npx vitest run` — all tests pass +- Forest screen has SVG background visible with grid overlay +- Elements from IndexedDB render as images with random offset + + + +1. Forest screen shows SVG woodland background (sky, hills, trees, clouds) +2. 4x3 CSS grid overlays the SVG for element placement +3. All stored ForestElements load from IndexedDB and display as images +4. New elements animate with fade-in + scale-up (1.2s ease-out) +5. Object URLs properly managed (revoked on re-render) +6. Level map still visible below/alongside the scene + + + +After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-03-SUMMARY.md` + diff --git a/.planning/phases/03-komplettes-spielerlebnis/03-04-PLAN.md b/.planning/phases/03-komplettes-spielerlebnis/03-04-PLAN.md new file mode 100644 index 0000000..fcb0131 --- /dev/null +++ b/.planning/phases/03-komplettes-spielerlebnis/03-04-PLAN.md @@ -0,0 +1,515 @@ +--- +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" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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; +export function getStyleReference(db: IDBDatabase): Promise; + + +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 }) => void, + onCancel?: () => void, + isReview?: boolean, showEncouragement?: boolean, +): () => void; + + +export interface Progress { + ... + totalCorrect: number; + totalErrors: number; + errorKeyCounts: Record; + 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 = ''; + 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 // returns saved element ID + ``` + + 1. Show placeholder immediately: set `#reward-image` area to placeholder HTML: + ```html +
+ 🍃 + 🍂 + 🍃 +

Der Wald denkt nach...

+
+ ``` + 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 `` 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 `
`: + ```html +
+
+

Schau mal!

+
+ +
+
+ +

+
+
+ + +
+
+
+ ``` + + **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 } + ): Promise + ``` + + 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 + + + +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 + + + +After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-04-SUMMARY.md` + diff --git a/.planning/phases/03-komplettes-spielerlebnis/03-05-PLAN.md b/.planning/phases/03-komplettes-spielerlebnis/03-05-PLAN.md new file mode 100644 index 0000000..5832a5d --- /dev/null +++ b/.planning/phases/03-komplettes-spielerlebnis/03-05-PLAN.md @@ -0,0 +1,483 @@ +--- +phase: 03-komplettes-spielerlebnis +plan: 05 +type: execute +wave: 2 +depends_on: [03-01] +files_modified: + - src/ui/parent.ts + - index.html + - src/styles/main.css + - src/app.ts +autonomous: true +requirements: [PRNT-01, PRNT-02, PRNT-03, PRNT-04] +must_haves: + truths: + - "Ctrl+Shift+E opens a code input prompt" + - "Entering '1234' navigates to parent area screen" + - "Parent area shows current level, completed sessions, practice days as calendar dots" + - "Parent area shows average accuracy and most frequent error keys" + - "Settings allow changing layout, toggling audio, changing companion, entering API key" + artifacts: + - path: "src/ui/parent.ts" + provides: "initParentScreen() with stats display and settings panel" + exports: ["initParentScreen", "showParentCodePrompt"] + - path: "index.html" + provides: "Parent screen HTML with stats overview and settings form" + contains: "parent-stats" + key_links: + - from: "src/ui/parent.ts" + to: "src/storage/db.ts" + via: "getProgress, saveProgress, getSettings, saveSettings" + pattern: "getProgress" + - from: "src/app.ts" + to: "src/ui/parent.ts" + via: "showParentCodePrompt on Ctrl+Shift+E" + pattern: "showParentCodePrompt" +--- + + +Build the parent area: gated access via Ctrl+Shift+E + code "1234", stats overview (level, sessions, practice days, accuracy, error keys), and settings (layout, audio, companion, API key). + +Purpose: Parents can see progress and adjust settings without the child being exposed to performance metrics. +Output: Parent screen with stats and settings, access gate wired into app.ts. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md +@src/app.ts +@src/storage/db.ts +@src/types.ts +@src/companion/characters.ts +@index.html +@src/styles/main.css + + + +export interface Progress { + id: 1; + currentLevel: number; + completedLevels: number[]; + totalSessions: number; + sessionDates: string[]; // ISO date strings "YYYY-MM-DD" + selectedCharacter: CompanionType; + selectedLayout: KeyboardLayout; + totalCorrect: number; + totalErrors: number; + errorKeyCounts: Record; + lastPlayedLevels: number[]; +} + +export interface Settings { + id: 1; + audioEnabled: boolean; + apiKey: string; + apiCallsToday: { text: number; image: number }; + lastApiCallDate: string; +} + + +export function getProgress(db: IDBDatabase): Promise; +export function saveProgress(db: IDBDatabase, progress: Progress): Promise; +export function getSettings(db: IDBDatabase): Promise; +export function saveSettings(db: IDBDatabase, settings: Settings): Promise; + + +export interface CompanionDefinition { type: CompanionType; name: string; emoji: string; personality: string; avatarUrl: string; } +export const companions: CompanionDefinition[]; + + +export function showScreen(name: ScreenName): void; +export function getDB(): IDBDatabase; + + + + + + + Task 1: Parent area HTML, CSS, and access gate + index.html, src/styles/main.css, src/ui/parent.ts + index.html, src/styles/main.css, src/types.ts, src/storage/db.ts, src/companion/characters.ts + + **Update index.html** — replace empty `
` (per D-14, D-17, D-18): + + ```html +
+
+ +

Elternbereich

+ + +
+

Bitte Code eingeben:

+ + +
+ + + +
+
+ ``` + + **Add CSS to `src/styles/main.css`:** + ```css + /* Parent Area */ + .parent { + max-width: 600px; + margin: 0 auto; + padding: 1.5rem; + } + + .parent__back-btn { + background: none; + border: none; + font-family: var(--font-heading); + font-size: 1rem; + color: var(--text-warm); + cursor: pointer; + margin-bottom: 1rem; + } + + .parent__title { + font-family: var(--font-heading); + color: var(--text-dark); + margin-bottom: 1.5rem; + } + + .parent__gate { + text-align: center; + padding: 2rem; + font-family: var(--font-body); + } + + .parent__code-input { + font-size: 2rem; + text-align: center; + width: 120px; + padding: 0.5rem; + border: 2px solid var(--accent-purple); + border-radius: 8px; + font-family: var(--font-heading); + letter-spacing: 0.5rem; + margin-top: 0.5rem; + } + + .parent__gate-error { + color: var(--accent-pink); + margin-top: 0.5rem; + font-size: 0.9rem; + } + + .parent__section { + margin-bottom: 2rem; + } + + .parent__section h2 { + font-family: var(--font-heading); + color: var(--text-dark); + border-bottom: 2px solid var(--bg-forest); + padding-bottom: 0.5rem; + margin-bottom: 1rem; + } + + .parent__stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; + } + + .parent__stat { + background: var(--bg-forest); + border-radius: 12px; + padding: 1rem; + text-align: center; + } + + .parent__stat-label { + display: block; + font-family: var(--font-body); + font-size: 0.85rem; + color: var(--text-warm); + margin-bottom: 0.25rem; + } + + .parent__stat-value { + display: block; + font-family: var(--font-heading); + font-size: 1.5rem; + font-weight: 700; + color: var(--text-dark); + } + + .parent__calendar-dots { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 0.5rem; + } + + .parent__calendar-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent-green); + } + + .parent__calendar-dot--empty { + background: var(--bg-cream); + border: 1px solid var(--text-light); + } + + .parent__errors h3, + .parent__calendar h3 { + font-family: var(--font-heading); + font-size: 1rem; + color: var(--text-warm); + margin-bottom: 0.5rem; + } + + .parent__error-key { + display: inline-block; + background: var(--accent-pink); + color: white; + padding: 0.25rem 0.75rem; + border-radius: 8px; + margin: 0.25rem; + font-family: var(--font-heading); + font-size: 0.9rem; + } + + .parent__settings { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .parent__setting { + display: flex; + align-items: center; + gap: 1rem; + } + + .parent__setting label:first-child { + min-width: 120px; + font-family: var(--font-body); + color: var(--text-warm); + } + + .parent__setting select, + .parent__input { + font-family: var(--font-body); + padding: 0.5rem; + border: 1px solid var(--text-light); + border-radius: 8px; + font-size: 0.95rem; + } + + .parent__save-btn { + font-family: var(--font-heading); + padding: 0.5rem 1rem; + background: var(--accent-green); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + } + ``` + + **Create `src/ui/parent.ts`:** + + ```typescript + import { getProgress, saveProgress, getSettings, saveSettings } from '../storage/db'; + import { companions } from '../companion/characters'; + import type { CompanionType, KeyboardLayout } from '../types'; + import { showScreen, getDB } from '../app'; + ``` + + **`showParentCodePrompt()`** (per D-14, PRNT-01): + - Show parent screen: `showScreen('parent')` + - Show gate, hide content + - Focus code input + - On input change, if value === '1234': hide gate, show content, call `loadParentData()` + - If value.length === 4 but wrong: show error, clear after 1.5s + + **`initParentScreen(db: IDBDatabase, onBack: () => void)`:** + Wire back button to `onBack` callback. + + **`loadParentData()`** (per D-17, PRNT-02, PRNT-03): + 1. Get progress from IndexedDB + 2. Set `#stat-level` to `progress.currentLevel` + 3. Set `#stat-sessions` to `progress.totalSessions` + 4. Calculate accuracy: `progress.totalCorrect + progress.totalErrors > 0 ? Math.round(progress.totalCorrect / (progress.totalCorrect + progress.totalErrors) * 100) : 0` + 5. Set `#stat-accuracy` to `${accuracy}%` + 6. Render calendar dots in `#calendar-dots`: + - Get last 30 days as date strings + - For each day: if in `progress.sessionDates`, render green dot, else empty dot + 7. Render error keys in `#error-keys-list`: + - Sort `progress.errorKeyCounts` by value descending + - Show top 5 keys as colored badges: `E (12x)` + - If no errors: show "Noch keine Fehler erfasst" + + **Settings (per D-18, PRNT-04):** + 1. Populate companion select from `companions` array + 2. Set current values from progress (layout, companion) and settings (audio, apiKey) + 3. On layout change: `saveProgress(db, { ...progress, selectedLayout: value })` + 4. On companion change: `saveProgress(db, { ...progress, selectedCharacter: value })` + 5. On audio toggle: `saveSettings(db, { ...settings, audioEnabled: checked })` + 6. On save API key: `saveSettings(db, { ...settings, apiKey: value })` + 7. Each save shows brief "Gespeichert!" feedback + + + npx tsc --noEmit && grep -c "showParentCodePrompt" src/ui/parent.ts && grep -c "parent-gate" index.html && grep -c "parent__calendar-dot" src/styles/main.css + + + - grep "showParentCodePrompt" src/ui/parent.ts returns export + - grep "1234" src/ui/parent.ts returns match (access code) + - grep "parent-gate" index.html returns match (gate overlay) + - grep "stat-level" index.html returns match (stats display) + - grep "stat-accuracy" index.html returns match + - grep "calendar-dots" index.html returns match + - grep "error-keys-list" index.html returns match + - grep "setting-layout" index.html returns match (layout setting) + - grep "setting-companion" index.html returns match (companion setting) + - grep "setting-apikey" index.html returns match (API key setting) + - grep "parent__calendar-dot" src/styles/main.css returns match + - grep "parent__error-key" src/styles/main.css returns match + - npx tsc --noEmit exits 0 + + Parent area gate with code "1234", stats overview with level/sessions/accuracy/calendar-dots/error-keys, settings for layout/audio/companion/API-key. All styled and functional. + + + + Task 2: Wire parent area into app.ts + src/app.ts + src/app.ts, src/ui/parent.ts + + **Wire parent area access in `src/app.ts`** (per D-14, PRNT-01): + + 1. Import: `import { showParentCodePrompt, initParentScreen } from './ui/parent';` + + 2. In `initApp()`, after DB is opened, call: + ```typescript + initParentScreen(db, async () => { + // Back from parent -> return to forest + await initForestScreen(db, startLessonFromForest); + showScreen('forest'); + }); + ``` + + 3. Replace the stub `showParentCodePrompt` reference (if Plan 04 added a stub) or add the global keydown listener: + ```typescript + document.addEventListener('keydown', (e) => { + if (e.ctrlKey && e.shiftKey && e.key === 'E') { + e.preventDefault(); + showParentCodePrompt(); + } + }); + ``` + Add this in `initApp()` after DB setup. Ensure it's only added once. + + 4. Make sure `showParentCodePrompt` properly uses the DB reference. Since `parent.ts` imports `getDB` from `app.ts`, it can access the DB. + + + npx tsc --noEmit && grep -c "showParentCodePrompt" src/app.ts && grep -c "initParentScreen" src/app.ts + + + - grep "import.*showParentCodePrompt" src/app.ts returns match + - grep "import.*initParentScreen" src/app.ts returns match + - grep "ctrlKey.*shiftKey" src/app.ts returns match (keyboard shortcut) + - grep "initParentScreen" src/app.ts shows it's called in initApp + - npx tsc --noEmit exits 0 + + Parent area fully wired: Ctrl+Shift+E triggers code prompt, "1234" opens parent screen, back button returns to forest. Global listener registered once in initApp. + + + + + +- `npx tsc --noEmit` — no type errors +- `npx vitest run` — all tests pass +- Parent area accessible via Ctrl+Shift+E + "1234" +- Stats show current data, settings persist changes + + + +1. Ctrl+Shift+E opens code input overlay on parent screen +2. Code "1234" reveals parent content, wrong code shows error +3. Overview shows: current level, total sessions, accuracy percentage +4. Calendar dots show last 30 days with green dots for practice days +5. Error keys shown as badges, sorted by frequency, top 5 +6. Settings: layout change, audio toggle, companion change, API key save — all persist to IndexedDB +7. Back button returns to forest screen + + + +After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-05-SUMMARY.md` +