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>
This commit is contained in:
+10
-2
@@ -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 | - |
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@src/types.ts
|
||||
@src/game/levels.ts
|
||||
@src/api/gemini.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Word lists per level with validation tests</name>
|
||||
<files>src/game/words.ts, src/game/words.test.ts</files>
|
||||
<read_first>src/game/levels.ts, src/types.ts</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
Create `src/game/words.ts` (per D-04):
|
||||
|
||||
```typescript
|
||||
import { getKeysUpToLevel } from './levels';
|
||||
|
||||
export const wordsByLevel: Record<number, string[]> = {
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/game/words.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Word lists for levels 1-6 exist, each with 8+ words using only learned letters, max 5 chars. Tests pass.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Extend Progress type and update rate limits</name>
|
||||
<files>src/types.ts, src/api/gemini.ts</files>
|
||||
<read_first>src/types.ts, src/api/gemini.ts</read_first>
|
||||
<action>
|
||||
**Progress type extension (per D-15):**
|
||||
Add these fields to the `Progress` interface in `src/types.ts`:
|
||||
```typescript
|
||||
totalCorrect: number;
|
||||
totalErrors: number;
|
||||
errorKeyCounts: Record<string, number>;
|
||||
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: [],
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Progress has stats fields, LessonPhase type exists, rate limits updated to 10 text / 5 image per day. TypeScript compiles clean.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npx vitest run` — all tests pass
|
||||
- `npx tsc --noEmit` — no type errors
|
||||
- Word lists validated: every word uses only learned letters for its level
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md
|
||||
@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
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/types.ts (after Plan 01): -->
|
||||
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<string, number>;
|
||||
lastPlayedLevels: number[];
|
||||
}
|
||||
|
||||
<!-- From src/game/words.ts (after Plan 01): -->
|
||||
export const wordsByLevel: Record<number, string[]>;
|
||||
export function getWordsForLevel(level: number, count: number): string[];
|
||||
|
||||
<!-- From src/companion/companion.ts (existing): -->
|
||||
export async function getLetterIntro(characterType: CompanionType, letter: string, fingerDescription: string, db: IDBDatabase): Promise<string>;
|
||||
|
||||
<!-- From src/game/typing.ts (existing): -->
|
||||
export function createExerciseState(level: number): TypingExerciseState;
|
||||
export function handleKeyPress(state: TypingExerciseState, pressedKey: string): KeyPressResult;
|
||||
export function getCurrentLetter(state: TypingExerciseState): string | null;
|
||||
|
||||
<!-- From src/game/levels.ts (existing): -->
|
||||
export function getLevelByNumber(n: number): Level | undefined;
|
||||
export function getKeysUpToLevel(n: number): string[];
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Extend typing engine with word mode</name>
|
||||
<files>src/game/typing.ts, src/game/typing.test.ts</files>
|
||||
<read_first>src/game/typing.ts, src/game/words.ts, src/types.ts</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/game/typing.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Typing engine supports word mode with word boundaries tracking. Backwards compatible with letter mode. Tests pass.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Lesson screen 3-phase state machine with review and repetition</name>
|
||||
<files>src/ui/screens.ts, index.html</files>
|
||||
<read_first>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</read_first>
|
||||
<action>
|
||||
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
|
||||
<div class="lesson__phase-indicator" id="lesson-phase-indicator"></div>
|
||||
```
|
||||
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<string, number> }) => 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
|
||||
<div class="word-display">
|
||||
<span class="word-display__letter word-display__letter--done">e</span>
|
||||
<span class="word-display__letter word-display__letter--active">i</span>
|
||||
<span class="word-display__letter">s</span>
|
||||
</div>
|
||||
```
|
||||
- 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<string, number>` 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); }
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && npx vitest run</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@src/forest/scene.ts
|
||||
@src/storage/db.ts
|
||||
@src/types.ts
|
||||
@index.html
|
||||
@src/styles/main.css
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/storage/db.ts (existing): -->
|
||||
export function getForestElements(db: IDBDatabase): Promise<ForestElement[]>;
|
||||
export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise<number>;
|
||||
|
||||
<!-- From src/types.ts (existing): -->
|
||||
export interface ForestElement {
|
||||
id?: number;
|
||||
levelCompleted: number;
|
||||
imageBlob: Blob;
|
||||
imagePrompt: string;
|
||||
description: string;
|
||||
companionText: string;
|
||||
position: { x: number; y: number };
|
||||
createdAt: string;
|
||||
}
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: SVG forest background and grid HTML structure</name>
|
||||
<files>index.html, src/styles/main.css</files>
|
||||
<read_first>index.html, src/styles/main.css, src/forest/scene.ts</read_first>
|
||||
<action>
|
||||
**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
|
||||
<div class="forest__scene" id="forest-scene">
|
||||
<svg class="forest__bg" viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Ground -->
|
||||
<rect x="0" y="300" width="800" height="100" fill="#C8E6C0" rx="0"/>
|
||||
<!-- Hills -->
|
||||
<ellipse cx="200" cy="320" rx="250" ry="80" fill="#A8D8A8"/>
|
||||
<ellipse cx="650" cy="330" rx="200" ry="60" fill="#B8E0B0"/>
|
||||
<!-- Trees (background) -->
|
||||
<g opacity="0.3">
|
||||
<ellipse cx="80" cy="240" rx="40" ry="60" fill="#7DB87D"/>
|
||||
<ellipse cx="720" cy="250" rx="35" ry="55" fill="#6DA86D"/>
|
||||
<ellipse cx="400" cy="230" rx="45" ry="70" fill="#8DC88D"/>
|
||||
</g>
|
||||
<!-- Sky gradient -->
|
||||
<defs>
|
||||
<linearGradient id="sky-grad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#E8F4FD"/>
|
||||
<stop offset="100%" stop-color="#F0F8E8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="800" height="300" fill="url(#sky-grad)"/>
|
||||
<!-- Clouds -->
|
||||
<ellipse cx="150" cy="60" rx="60" ry="25" fill="white" opacity="0.5"/>
|
||||
<ellipse cx="550" cy="80" rx="70" ry="20" fill="white" opacity="0.4"/>
|
||||
<!-- Sun -->
|
||||
<circle cx="700" cy="50" r="30" fill="#F8E8A0" opacity="0.6"/>
|
||||
</svg>
|
||||
<div class="forest__grid" id="forest-grid">
|
||||
<!-- 12 slots for forest elements, filled dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c "forest-scene" index.html && grep -c "forest__grid" index.html && grep -c "forest__element--new" src/styles/main.css</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Forest scene has SVG background with woodland scenery, 4x3 CSS grid overlay, and fade-in+scale-up animation keyframes for new elements.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Render forest elements from IndexedDB</name>
|
||||
<files>src/forest/scene.ts</files>
|
||||
<read_first>src/forest/scene.ts, src/storage/db.ts, src/types.ts, index.html</read_first>
|
||||
<action>
|
||||
Extend `src/forest/scene.ts` (per D-07, D-08, FRST-03):
|
||||
|
||||
Add a new exported function `renderForestScene(db: IDBDatabase, newElementId?: number): Promise<void>`:
|
||||
|
||||
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 `<div class="forest__element">`
|
||||
- Create an `<img>` 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && npx vitest run</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire the complete game flow: lesson -> reward -> forest. Build reward screen with pre-generated forest element image, companion comment, and navigation buttons. Connect review/repetition triggers in app.ts.
|
||||
|
||||
Purpose: This closes the game loop — the child finishes a lesson, sees a magical reward, and returns to a growing forest.
|
||||
Output: Reward module, reward screen HTML/CSS, updated app.ts with full flow including review/repetition logic.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md
|
||||
@.planning/phases/03-komplettes-spielerlebnis/03-02-SUMMARY.md
|
||||
@.planning/phases/03-komplettes-spielerlebnis/03-03-SUMMARY.md
|
||||
@src/app.ts
|
||||
@src/api/gemini.ts
|
||||
@src/companion/companion.ts
|
||||
@src/storage/db.ts
|
||||
@src/forest/scene.ts
|
||||
@src/types.ts
|
||||
@index.html
|
||||
@src/companion/fallbacks.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/api/gemini.ts (existing): -->
|
||||
export async function generateImage(prompt: string, referenceImages: Blob[], config: GeminiConfig): Promise<Blob | null>;
|
||||
export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise<string | null>;
|
||||
export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise<boolean>;
|
||||
export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise<void>;
|
||||
|
||||
<!-- From src/storage/db.ts (existing): -->
|
||||
export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise<number>;
|
||||
export function getForestElements(db: IDBDatabase): Promise<ForestElement[]>;
|
||||
export function getStyleReference(db: IDBDatabase): Promise<StyleReference | null>;
|
||||
|
||||
<!-- From src/companion/companion.ts (existing): -->
|
||||
export async function getForestComment(characterType: CompanionType, elementDescription: string, db: IDBDatabase): Promise<string>;
|
||||
|
||||
<!-- From src/companion/fallbacks.ts (existing): -->
|
||||
export function getRandomFallbackForestComment(): string;
|
||||
import { getRandomFallbackImage } from './fallbacks'; // returns SVG blob
|
||||
|
||||
<!-- From src/forest/scene.ts (after Plan 03): -->
|
||||
export async function renderForestScene(db: IDBDatabase, newElementId?: number): Promise<void>;
|
||||
export async function initForestScreen(db: IDBDatabase, onStartLesson: (level: number) => void): Promise<void>;
|
||||
|
||||
<!-- From src/ui/screens.ts (after Plan 02): -->
|
||||
export function initLessonScreen(
|
||||
db: IDBDatabase, level: number, layout: KeyboardLayout,
|
||||
onComplete: (level: number, stats: { totalCorrect: number; totalErrors: number; errorKeys: Record<string, number> }) => void,
|
||||
onCancel?: () => void,
|
||||
isReview?: boolean, showEncouragement?: boolean,
|
||||
): () => void;
|
||||
|
||||
<!-- From src/types.ts (after Plan 01): -->
|
||||
export interface Progress {
|
||||
...
|
||||
totalCorrect: number;
|
||||
totalErrors: number;
|
||||
errorKeyCounts: Record<string, number>;
|
||||
lastPlayedLevels: number[];
|
||||
}
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Reward module with pre-generation and reward screen</name>
|
||||
<files>src/forest/reward.ts, index.html, src/styles/main.css</files>
|
||||
<read_first>src/api/gemini.ts, src/storage/db.ts, src/companion/companion.ts, src/companion/fallbacks.ts, src/types.ts, index.html, src/styles/main.css</read_first>
|
||||
<action>
|
||||
**Create `src/forest/reward.ts`** (per D-10, D-11, D-12):
|
||||
|
||||
```typescript
|
||||
import { loadConfig } from '../api/config';
|
||||
import { generateImage, checkRateLimit, incrementApiCall } from '../api/gemini';
|
||||
import { getStyleReference, saveForestElement, getForestElements } from '../storage/db';
|
||||
import { getForestComment } from '../companion/companion';
|
||||
import { getRandomFallbackForestComment } from '../companion/fallbacks';
|
||||
import type { CompanionType, ForestElement } from '../types';
|
||||
```
|
||||
|
||||
**Pre-generation (per D-11, RWRD-02):**
|
||||
```typescript
|
||||
let pregenPromise: Promise<{ blob: Blob; description: string }> | null = null;
|
||||
|
||||
export function startPreGeneration(db: IDBDatabase): void {
|
||||
pregenPromise = generateForestElement(db);
|
||||
}
|
||||
```
|
||||
|
||||
`generateForestElement(db)` async function:
|
||||
1. Check rate limit for 'image': `await checkRateLimit(db, 'image')`
|
||||
2. If rate-limited, return fallback: use `getRandomFallbackImage()` from fallbacks.ts
|
||||
- NOTE: Check if `getRandomFallbackImage` exists. If not, create inline fallback: load one of the SVGs from `src/assets/fallback-images/` via fetch.
|
||||
- Actually, look at existing fallbacks.ts — it has `getRandomFallbackImage` or similar. If not, we need a simple function that picks a random fallback SVG and returns it as a Blob.
|
||||
3. Load config via `loadConfig()`. If no config, use fallback.
|
||||
4. Get style reference via `getStyleReference(db)`. Build referenceImages array (style ref blob if available).
|
||||
5. Pick random forest element type from a list: `['Blume', 'Pilz', 'Schmetterling', 'Vogel', 'Reh', 'Eichhoernchen', 'Hase', 'Frosch', 'Igel', 'Marienkaefer', 'Libelle', 'Schnecke']`
|
||||
6. Build prompt: `"Kinderbuch-Illustration, Aquarell-Stil, ${elementType}, magischer Wald, warm, einladend, fuer Kinder, Pastellfarben, kein Text, einzelnes Element auf transparentem Hintergrund"`
|
||||
7. Call `generateImage(prompt, referenceImages, config)`
|
||||
8. If result null, use fallback
|
||||
9. Increment API call: `await incrementApiCall(db, 'image')`
|
||||
10. Return `{ blob, description: elementType }`
|
||||
|
||||
**Fallback image function:**
|
||||
Add to reward.ts:
|
||||
```typescript
|
||||
const FALLBACK_SVGS = [
|
||||
'/src/assets/fallback-images/blume.svg',
|
||||
'/src/assets/fallback-images/pilz.svg',
|
||||
'/src/assets/fallback-images/vogel.svg',
|
||||
'/src/assets/fallback-images/schmetterling.svg',
|
||||
'/src/assets/fallback-images/reh.svg',
|
||||
'/src/assets/fallback-images/baum.svg',
|
||||
'/src/assets/fallback-images/bach.svg',
|
||||
];
|
||||
|
||||
async function getRandomFallbackImage(): Promise<{ blob: Blob; description: string }> {
|
||||
const idx = Math.floor(Math.random() * FALLBACK_SVGS.length);
|
||||
const path = FALLBACK_SVGS[idx]!;
|
||||
const name = path.split('/').pop()!.replace('.svg', '');
|
||||
try {
|
||||
const res = await fetch(path);
|
||||
const blob = await res.blob();
|
||||
return { blob, description: name };
|
||||
} catch {
|
||||
// Ultimate fallback: empty SVG blob
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#E8F5E4"/></svg>';
|
||||
return { blob: new Blob([svg], { type: 'image/svg+xml' }), description: 'element' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**initRewardScreen (per RWRD-01, RWRD-03, RWRD-04):**
|
||||
```typescript
|
||||
export async function initRewardScreen(
|
||||
db: IDBDatabase,
|
||||
level: number,
|
||||
characterType: CompanionType,
|
||||
onContinue: () => void,
|
||||
onBackToForest: () => void,
|
||||
): Promise<number> // returns saved element ID
|
||||
```
|
||||
|
||||
1. Show placeholder immediately: set `#reward-image` area to placeholder HTML:
|
||||
```html
|
||||
<div class="reward__placeholder">
|
||||
<span class="reward__leaf reward__leaf--1">🍃</span>
|
||||
<span class="reward__leaf reward__leaf--2">🍂</span>
|
||||
<span class="reward__leaf reward__leaf--3">🍃</span>
|
||||
<p>Der Wald denkt nach...</p>
|
||||
</div>
|
||||
```
|
||||
2. Await `pregenPromise` (or `generateForestElement(db)` if promise is null)
|
||||
3. Get companion comment: `await getForestComment(characterType, result.description, db)`
|
||||
4. Determine grid position for new element: count existing elements, position = `{ x: col, y: row }` based on next free slot in 4x3 grid (slot index = existingCount % 12). Add random offset +-15px.
|
||||
5. Save to IndexedDB:
|
||||
```typescript
|
||||
const element: ForestElement = {
|
||||
levelCompleted: level,
|
||||
imageBlob: result.blob,
|
||||
imagePrompt: prompt,
|
||||
description: result.description,
|
||||
companionText: comment,
|
||||
position: { x: col, y: row },
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const elementId = await saveForestElement(db, element);
|
||||
```
|
||||
6. Replace placeholder with actual image: `URL.createObjectURL(result.blob)` in `<img>` tag
|
||||
7. Show companion comment in `#reward-companion-text`
|
||||
8. Wire buttons: `#reward-continue-btn` -> onContinue, `#reward-back-btn` -> onBackToForest
|
||||
9. Return elementId (for forest scene animation)
|
||||
|
||||
**Update index.html** — replace empty `<section id="screen-reward">`:
|
||||
```html
|
||||
<section id="screen-reward" class="screen">
|
||||
<div class="reward">
|
||||
<h2 class="reward__title">Schau mal!</h2>
|
||||
<div class="reward__image-area" id="reward-image-area">
|
||||
<!-- Placeholder or generated image -->
|
||||
</div>
|
||||
<div class="reward__companion" id="reward-companion">
|
||||
<img id="reward-companion-avatar" class="companion-area__avatar" src="" alt="">
|
||||
<p id="reward-companion-text" class="companion-area__text"></p>
|
||||
</div>
|
||||
<div class="reward__actions">
|
||||
<button class="reward__btn reward__btn--continue" id="reward-continue-btn">Weiter ueben</button>
|
||||
<button class="reward__btn reward__btn--back" id="reward-back-btn">Zurueck zum Wald</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Add CSS to `src/styles/main.css`:**
|
||||
```css
|
||||
/* Reward Screen */
|
||||
.reward {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.reward__title {
|
||||
font-family: var(--font-heading);
|
||||
color: var(--accent-gold);
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.reward__image-area {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 16px;
|
||||
background: var(--bg-forest);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reward__image-area img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
animation: reward-image-appear 0.8s ease-out;
|
||||
}
|
||||
|
||||
@keyframes reward-image-appear {
|
||||
from { opacity: 0; transform: scale(0.5); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.reward__placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-warm);
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
.reward__leaf {
|
||||
font-size: 1.5rem;
|
||||
animation: leaf-float 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.reward__leaf--1 { animation-delay: 0s; }
|
||||
.reward__leaf--2 { animation-delay: 0.5s; }
|
||||
.reward__leaf--3 { animation-delay: 1s; }
|
||||
|
||||
@keyframes leaf-float {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-10px) rotate(15deg); }
|
||||
}
|
||||
|
||||
.reward__companion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.reward__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.reward__btn {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.reward__btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.reward__btn--continue {
|
||||
background: var(--accent-green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reward__btn--back {
|
||||
background: var(--bg-cream);
|
||||
color: var(--text-warm);
|
||||
border: 2px solid var(--accent-green);
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && grep -c "startPreGeneration" src/forest/reward.ts && grep -c "initRewardScreen" src/forest/reward.ts && grep -c "reward-continue-btn" index.html</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep "startPreGeneration" src/forest/reward.ts returns export
|
||||
- grep "initRewardScreen" src/forest/reward.ts returns export
|
||||
- grep "generateImage" src/forest/reward.ts shows image generation call
|
||||
- grep "saveForestElement" src/forest/reward.ts shows persistence
|
||||
- grep "Der Wald denkt nach" src/forest/reward.ts shows placeholder text
|
||||
- grep "reward-continue-btn" index.html returns match
|
||||
- grep "reward-back-btn" index.html returns match
|
||||
- grep "reward__placeholder" src/styles/main.css returns match
|
||||
- grep "leaf-float" src/styles/main.css returns match (animated leaves)
|
||||
- npx tsc --noEmit exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Reward module with pre-generation, placeholder, fallback, IndexedDB persistence. Reward screen HTML with image area, companion comment, two action buttons. All CSS styled.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire complete flow in app.ts with review/repeat triggers</name>
|
||||
<files>src/app.ts</files>
|
||||
<read_first>src/app.ts, src/forest/reward.ts, src/forest/scene.ts, src/ui/screens.ts, src/types.ts, src/storage/db.ts, src/companion/characters.ts</read_first>
|
||||
<action>
|
||||
Rewrite flow logic in `src/app.ts` (per D-10, D-11, D-05, D-06, TYPE-07):
|
||||
|
||||
**Import additions:**
|
||||
```typescript
|
||||
import { startPreGeneration, initRewardScreen } from './forest/reward';
|
||||
import { renderForestScene } from './forest/scene';
|
||||
```
|
||||
|
||||
**Update `startLessonFromForest(level: number)`:**
|
||||
1. Determine if this is a review session (per D-05): `progress.totalSessions > 0 && progress.totalSessions % 3 === 0`
|
||||
2. Determine if encouragement needed (per D-06): check `progress.lastPlayedLevels` — if last 2 entries are the same level as current, set `showEncouragement = true`
|
||||
3. Start pre-generation: `startPreGeneration(db)` — fire and forget (per D-11)
|
||||
4. Call updated `initLessonScreen(db, level, layout, handleLessonComplete, handleLessonCancel, isReview, showEncouragement)`
|
||||
|
||||
**Update `handleLessonComplete`:**
|
||||
Change signature to accept stats:
|
||||
```typescript
|
||||
async function handleLessonComplete(
|
||||
completedLevel: number,
|
||||
stats: { totalCorrect: number; totalErrors: number; errorKeys: Record<string, number> }
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
1. Update progress with stats (per D-15, D-16):
|
||||
```typescript
|
||||
progress.totalCorrect += stats.totalCorrect;
|
||||
progress.totalErrors += stats.totalErrors;
|
||||
for (const [key, count] of Object.entries(stats.errorKeys)) {
|
||||
progress.errorKeyCounts[key] = (progress.errorKeyCounts[key] ?? 0) + count;
|
||||
}
|
||||
// Track last played levels (keep last 3)
|
||||
progress.lastPlayedLevels.push(completedLevel);
|
||||
if (progress.lastPlayedLevels.length > 3) {
|
||||
progress.lastPlayedLevels = progress.lastPlayedLevels.slice(-3);
|
||||
}
|
||||
```
|
||||
2. Existing level advancement logic (keep as-is)
|
||||
3. Save progress
|
||||
4. Show REWARD screen instead of going directly to forest:
|
||||
```typescript
|
||||
showScreen('reward');
|
||||
const elementId = await initRewardScreen(
|
||||
db,
|
||||
completedLevel,
|
||||
progress.selectedCharacter,
|
||||
() => {
|
||||
// "Weiter ueben" — start next lesson
|
||||
startLessonFromForest(Math.min(progress.currentLevel, MAX_LEVEL));
|
||||
},
|
||||
async () => {
|
||||
// "Zurueck zum Wald" — show forest with new element animated
|
||||
await initForestScreen(db, startLessonFromForest);
|
||||
await renderForestScene(db, elementId);
|
||||
showScreen('forest');
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
**Set companion avatar on reward screen:**
|
||||
After `showScreen('reward')`, set the reward companion avatar:
|
||||
```typescript
|
||||
const companion = companions.find(c => c.type === progress.selectedCharacter);
|
||||
if (companion) {
|
||||
const avatar = document.getElementById('reward-companion-avatar') as HTMLImageElement;
|
||||
avatar.src = companion.avatarUrl;
|
||||
avatar.alt = companion.name;
|
||||
}
|
||||
```
|
||||
|
||||
**Handle backward compatibility:**
|
||||
The `handleLessonComplete` in `startLessonFromForest` is passed as callback. Make sure the `initLessonScreen` onComplete signature matches (it was updated in Plan 02 to pass stats).
|
||||
|
||||
**Handle missing fields in existing progress data:**
|
||||
When loading progress, ensure new fields have defaults:
|
||||
```typescript
|
||||
if (!progress.errorKeyCounts) progress.errorKeyCounts = {};
|
||||
if (!progress.lastPlayedLevels) progress.lastPlayedLevels = [];
|
||||
if (progress.totalCorrect === undefined) progress.totalCorrect = 0;
|
||||
if (progress.totalErrors === undefined) progress.totalErrors = 0;
|
||||
```
|
||||
Add this migration logic after `getProgress(db)` calls.
|
||||
|
||||
**Parent area shortcut:**
|
||||
Add global keydown listener for Ctrl+Shift+E (this will be used by Plan 05, but wire the detection here):
|
||||
```typescript
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'E') {
|
||||
e.preventDefault();
|
||||
showParentCodePrompt();
|
||||
}
|
||||
});
|
||||
```
|
||||
For now, `showParentCodePrompt` can be a stub that will be implemented in Plan 05.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && npx vitest run</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep "startPreGeneration" src/app.ts returns match (pre-gen at lesson start)
|
||||
- grep "initRewardScreen" src/app.ts returns match (reward after lesson)
|
||||
- grep "showScreen.*reward" src/app.ts returns match (navigate to reward)
|
||||
- grep "renderForestScene" src/app.ts returns match (forest update after reward)
|
||||
- grep "totalSessions.*% 3" src/app.ts returns match (review trigger)
|
||||
- grep "lastPlayedLevels" src/app.ts returns match (repeat detection)
|
||||
- grep "errorKeyCounts" src/app.ts returns match (stats accumulation)
|
||||
- grep "Ctrl.*Shift.*E" src/app.ts or grep "ctrlKey.*shiftKey" src/app.ts returns match
|
||||
- npx tsc --noEmit exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Complete flow wired: forest -> lesson (with pre-gen) -> reward (with image + comment) -> forest (with animation). Review triggers every 3rd session. Repetition protection tracks last played levels. Stats accumulated to progress.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npx tsc --noEmit` — no type errors
|
||||
- `npx vitest run` — all tests pass
|
||||
- Full flow testable: start lesson -> complete 3 phases -> see reward -> navigate back
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. Reward screen shows generated/fallback image with companion comment after every lesson
|
||||
2. Pre-generation starts at lesson begin, reward screen awaits it with placeholder
|
||||
3. Placeholder shows "Der Wald denkt nach..." with animated leaves
|
||||
4. "Weiter ueben" starts next lesson, "Zurueck zum Wald" returns to forest
|
||||
5. New element saved to IndexedDB and animated in forest grid
|
||||
6. Every 3rd session triggers review mode
|
||||
7. After 2x same level, encouragement displayed
|
||||
8. Exercise stats (correct, errors, errorKeys) accumulated in Progress
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-04-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md
|
||||
@src/app.ts
|
||||
@src/storage/db.ts
|
||||
@src/types.ts
|
||||
@src/companion/characters.ts
|
||||
@index.html
|
||||
@src/styles/main.css
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/types.ts (after Plan 01): -->
|
||||
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<string, number>;
|
||||
lastPlayedLevels: number[];
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
id: 1;
|
||||
audioEnabled: boolean;
|
||||
apiKey: string;
|
||||
apiCallsToday: { text: number; image: number };
|
||||
lastApiCallDate: string;
|
||||
}
|
||||
|
||||
<!-- From src/storage/db.ts (existing): -->
|
||||
export function getProgress(db: IDBDatabase): Promise<Progress | null>;
|
||||
export function saveProgress(db: IDBDatabase, progress: Progress): Promise<void>;
|
||||
export function getSettings(db: IDBDatabase): Promise<Settings | null>;
|
||||
export function saveSettings(db: IDBDatabase, settings: Settings): Promise<void>;
|
||||
|
||||
<!-- From src/companion/characters.ts (existing): -->
|
||||
export interface CompanionDefinition { type: CompanionType; name: string; emoji: string; personality: string; avatarUrl: string; }
|
||||
export const companions: CompanionDefinition[];
|
||||
|
||||
<!-- From src/app.ts (existing): -->
|
||||
export function showScreen(name: ScreenName): void;
|
||||
export function getDB(): IDBDatabase;
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Parent area HTML, CSS, and access gate</name>
|
||||
<files>index.html, src/styles/main.css, src/ui/parent.ts</files>
|
||||
<read_first>index.html, src/styles/main.css, src/types.ts, src/storage/db.ts, src/companion/characters.ts</read_first>
|
||||
<action>
|
||||
**Update index.html** — replace empty `<section id="screen-parent">` (per D-14, D-17, D-18):
|
||||
|
||||
```html
|
||||
<section id="screen-parent" class="screen">
|
||||
<div class="parent">
|
||||
<button class="parent__back-btn" id="parent-back-btn">← Zurueck</button>
|
||||
<h1 class="parent__title">Elternbereich</h1>
|
||||
|
||||
<!-- Code gate overlay -->
|
||||
<div class="parent__gate" id="parent-gate">
|
||||
<p>Bitte Code eingeben:</p>
|
||||
<input type="password" id="parent-code-input" class="parent__code-input" maxlength="4" placeholder="____" inputmode="numeric">
|
||||
<p class="parent__gate-error" id="parent-gate-error" style="display:none">Falscher Code</p>
|
||||
</div>
|
||||
|
||||
<!-- Content (hidden until code entered) -->
|
||||
<div class="parent__content" id="parent-content" style="display:none">
|
||||
|
||||
<section class="parent__section">
|
||||
<h2>Uebersicht</h2>
|
||||
<div class="parent__stats" id="parent-stats">
|
||||
<div class="parent__stat">
|
||||
<span class="parent__stat-label">Aktuelle Stufe</span>
|
||||
<span class="parent__stat-value" id="stat-level">-</span>
|
||||
</div>
|
||||
<div class="parent__stat">
|
||||
<span class="parent__stat-label">Einheiten</span>
|
||||
<span class="parent__stat-value" id="stat-sessions">-</span>
|
||||
</div>
|
||||
<div class="parent__stat">
|
||||
<span class="parent__stat-label">Genauigkeit</span>
|
||||
<span class="parent__stat-value" id="stat-accuracy">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="parent__calendar" id="parent-calendar">
|
||||
<h3>Uebungstage</h3>
|
||||
<div class="parent__calendar-dots" id="calendar-dots"></div>
|
||||
</div>
|
||||
<div class="parent__errors" id="parent-errors">
|
||||
<h3>Haeufigste Fehlertasten</h3>
|
||||
<div id="error-keys-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="parent__section">
|
||||
<h2>Einstellungen</h2>
|
||||
<div class="parent__settings">
|
||||
<div class="parent__setting">
|
||||
<label>Tastatur-Layout</label>
|
||||
<select id="setting-layout">
|
||||
<option value="de">DE (Deutschland)</option>
|
||||
<option value="ch">CH (Schweiz)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="parent__setting">
|
||||
<label>Audio</label>
|
||||
<label class="parent__toggle">
|
||||
<input type="checkbox" id="setting-audio">
|
||||
<span>An/Aus</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="parent__setting">
|
||||
<label>Begleitfigur</label>
|
||||
<select id="setting-companion"></select>
|
||||
</div>
|
||||
<div class="parent__setting">
|
||||
<label>API-Key</label>
|
||||
<input type="password" id="setting-apikey" class="parent__input" placeholder="Gemini API Key">
|
||||
<button class="parent__save-btn" id="save-apikey-btn">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**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: `<span class="parent__error-key">E (12x)</span>`
|
||||
- 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
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire parent area into app.ts</name>
|
||||
<files>src/app.ts</files>
|
||||
<read_first>src/app.ts, src/ui/parent.ts</read_first>
|
||||
<action>
|
||||
**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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && grep -c "showParentCodePrompt" src/app.ts && grep -c "initParentScreen" src/app.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-05-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user