From 9624323c9bfd0810f1b017d5659a6afb054f7326 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Sun, 29 Mar 2026 10:55:52 +0200 Subject: [PATCH] =?UTF-8?q?docs(01):=20create=20phase=201=20plan=20?= =?UTF-8?q?=E2=80=94=205=20plans=20across=204=20waves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/ROADMAP.md | 12 +- .../01-grundger-st-tippmechanik/01-01-PLAN.md | 384 +++++++++++++++ .../01-grundger-st-tippmechanik/01-02-PLAN.md | 341 +++++++++++++ .../01-grundger-st-tippmechanik/01-03-PLAN.md | 407 ++++++++++++++++ .../01-grundger-st-tippmechanik/01-04-PLAN.md | 447 ++++++++++++++++++ .../01-grundger-st-tippmechanik/01-05-PLAN.md | 371 +++++++++++++++ 6 files changed, 1960 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/01-grundger-st-tippmechanik/01-01-PLAN.md create mode 100644 .planning/phases/01-grundger-st-tippmechanik/01-02-PLAN.md create mode 100644 .planning/phases/01-grundger-st-tippmechanik/01-03-PLAN.md create mode 100644 .planning/phases/01-grundger-st-tippmechanik/01-04-PLAN.md create mode 100644 .planning/phases/01-grundger-st-tippmechanik/01-05-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 51ce7c6..abeb3b1 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -29,7 +29,15 @@ Decimal phases appear between their surrounding integers in numeric order. 3. Stufe 1 (F, J, Leertaste) ist spielbar: Buchstaben erscheinen einzeln, richtiger Tastendruck loest sie auf, falscher Tastendruck tut nichts 4. Nach 10 richtigen Buchstaben gilt die Uebung als abgeschlossen und Stufe 2 wird freigeschaltet 5. Die Wald-Uebersicht zeigt den Stufenstatus (locked / current / completed) korrekt an -**Plans**: TBD +**Plans:** 5 plans + +Plans: +- [ ] 01-01-PLAN.md — Vite scaffold, project structure, CSS palette, TypeScript interfaces, IndexedDB wrapper +- [ ] 01-02-PLAN.md — Screen management, welcome screen, companion/layout selection, onboarding persistence +- [ ] 01-03-PLAN.md — QWERTZ keyboard component with finger colors, level definitions 1-6 +- [ ] 01-04-PLAN.md — Typing exercise engine (TDD), adaptive tempo, lesson screen UI +- [ ] 01-05-PLAN.md — Forest overview with level map, full app flow integration, human verification + **UI hint**: yes ### Phase 2: Gemini-Integration + Asset-Pipeline @@ -76,7 +84,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. Grundgerüst + Tippmechanik | 0/? | Not started | - | +| 1. Grundgerüst + Tippmechanik | 0/5 | Planning complete | - | | 2. Gemini-Integration + Asset-Pipeline | 0/? | Not started | - | | 3. Komplettes Spielerlebnis | 0/? | Not started | - | | 4. Polish + Audio | 0/? | Not started | - | diff --git a/.planning/phases/01-grundger-st-tippmechanik/01-01-PLAN.md b/.planning/phases/01-grundger-st-tippmechanik/01-01-PLAN.md new file mode 100644 index 0000000..4b61dc6 --- /dev/null +++ b/.planning/phases/01-grundger-st-tippmechanik/01-01-PLAN.md @@ -0,0 +1,384 @@ +--- +phase: 01-grundger-st-tippmechanik +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - tsconfig.json + - vite.config.ts + - biome.json + - index.html + - .gitignore + - src/main.ts + - src/types.ts + - src/storage/db.ts + - src/styles/main.css + - public/config.example.json +autonomous: true +requirements: + - FNDN-01 + - FNDN-02 + - FNDN-03 + - FNDN-04 + - PLSH-06 + +must_haves: + truths: + - "npm run dev startet Vite auf 0.0.0.0:5173 ohne Fehler" + - "Alle Verzeichnisse aus Spec Abschnitt 13 existieren" + - "TypeScript-Interfaces sind exportiert und typisiert" + - "IndexedDB oeffnet mit 5 Stores und schliesst nicht bei reload" + - "CSS Custom Properties fuer alle Farben und Fonts sind definiert" + artifacts: + - path: "package.json" + provides: "Project dependencies and scripts" + contains: "vite" + - path: "src/types.ts" + provides: "All shared TypeScript interfaces" + exports: ["Progress", "ForestElement", "CompanionAssets", "Level", "Settings", "StyleReference"] + - path: "src/storage/db.ts" + provides: "IndexedDB wrapper with 5 stores" + exports: ["openDB", "getProgress", "saveProgress", "getSettings", "saveSettings"] + - path: "src/styles/main.css" + provides: "CSS palette and typography" + contains: "--bg-cream" + key_links: + - from: "src/storage/db.ts" + to: "src/types.ts" + via: "imports Progress, Settings, ForestElement etc." + pattern: "import.*from.*types" + - from: "src/main.ts" + to: "src/storage/db.ts" + via: "initializes DB on app start" + pattern: "openDB" +--- + + +Scaffold the complete Vite project, define all TypeScript interfaces, create the IndexedDB wrapper, and establish the CSS design system. + +Purpose: Every subsequent plan depends on the project existing, types being defined, storage being available, and styles being set. +Output: A runnable Vite dev server with types, IndexedDB, and CSS custom properties — no visible UI yet beyond a blank styled page. + + + +@$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/01-grundger-st-tippmechanik/01-CONTEXT.md +@SPEC.md (sections 6.2, 6.3, 7.2, 13) + + + + + + Task 1: Vite project scaffold + project structure + CSS design system + + package.json, tsconfig.json, vite.config.ts, biome.json, index.html, + .gitignore, src/main.ts, src/styles/main.css, public/config.example.json + + + SPEC.md (section 13 for project structure, section 6.2 for tech stack, section 7.2 for CSS palette) + .planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md + + + 1. Initialize Vite project with vanilla-ts template: + `npm create vite@latest . -- --template vanilla-ts` (or manually create files if directory not empty). + + 2. Install dependencies: + - `npm install` (vite, typescript) + - `npm install -D vitest @biomejs/biome` + + 3. Configure vite.config.ts (per D-01, spec section 0): + ```typescript + import { defineConfig } from 'vite' + export default defineConfig({ + server: { host: '0.0.0.0', port: 5173 }, + preview: { host: '0.0.0.0', port: 4173 } + }) + ``` + + 4. Configure tsconfig.json: `"strict": true`, target ES2020, module ESNext, moduleResolution bundler. + + 5. Configure biome.json: enable formatter and linter, indent with tabs, line width 100. + + 6. Configure package.json scripts: + ```json + { + "dev": "vite --host", + "build": "tsc && vite build", + "preview": "vite preview --host --port 4173", + "test": "vitest run", + "test:watch": "vitest", + "lint": "biome check .", + "lint:fix": "biome check --write ." + } + ``` + + 7. Create .gitignore with: node_modules, dist, public/config.json, *.local + + 8. Create ALL directories from spec section 13: + - src/game/, src/forest/, src/companion/, src/api/, src/storage/, src/ui/, src/styles/ + - src/assets/companions/fee-lila/, src/assets/companions/einhorn-stella/, + src/assets/companions/fuchs-finn/, src/assets/companions/eule-elsa/ + - src/assets/style-reference/, src/assets/fallback-images/ + - scripts/ + - public/ + Place empty .gitkeep in asset directories that have no files yet. + + 9. Create public/config.example.json: + ```json + { + "geminiApiKey": "YOUR_API_KEY_HERE", + "geminiModel": "gemini-2.5-flash", + "imageModel": "gemini-3.1-flash-image-preview" + } + ``` + + 10. Create index.html with: + - DOCTYPE html, lang="de" + - Google Fonts link: Quicksand (400, 600, 700) + Nunito (400, 600) + - Link to src/styles/main.css (via Vite) + - 5 screen sections per D-01: `
`, + `
`, + `
`, + `
`, + `
` + - Only screen-welcome gets class `screen--active` initially + - `` + + 11. Create src/styles/main.css with ALL CSS custom properties from spec section 7.2: + ```css + :root { + --bg-cream: #FFF8F0; + --bg-forest: #E8F5E4; + --text-dark: #3D3225; + --text-warm: #5C4A3A; + --text-light: #8B7D6B; + --accent-green: #6DB87D; + --accent-gold: #E8B84B; + --accent-pink: #E88BAE; + --accent-purple: #9B7DC4; + --accent-blue: #7DB8D4; + --finger-l-pinky: #E8B4C8; + --finger-l-ring: #C4A8D4; + --finger-l-mid: #A8C4E0; + --finger-l-index: #A8D8B8; + --finger-r-index: #D4D8A8; + --finger-r-mid: #E0C4A8; + --finger-r-ring: #D4A8A8; + --finger-r-pinky: #C8B4E8; + --finger-thumb: #D4CFC8; + } + ``` + Body: font-family 'Nunito', sans-serif; background var(--bg-cream); color var(--text-dark); min font-size 16px; margin 0; padding 0. + Headings (h1–h3): font-family 'Quicksand', sans-serif. + Screen base: `.screen { display: none; opacity: 0; transition: opacity 0.3s ease; }` and `.screen--active { display: flex; flex-direction: column; align-items: center; opacity: 1; min-height: 100vh; }`. + + 12. Create src/main.ts as minimal entry: import './styles/main.css', console.log('Zauberwald loaded'). + + + cd /home/dev/workspace/zauberwald && npm run dev -- --strictPort 2>&1 & sleep 3 && curl -s http://localhost:5173 | grep -q "Zauberwald" || curl -s http://localhost:5173 | grep -q "screen-welcome" && kill %1 && echo "PASS" + + + - package.json contains "vite" in devDependencies and scripts "dev", "build", "preview", "test", "lint" + - vite.config.ts contains `host: '0.0.0.0'` and `port: 5173` + - tsconfig.json contains `"strict": true` + - biome.json exists and contains `"formatter"` and `"linter"` + - .gitignore contains `public/config.json` and `node_modules` + - index.html contains 5 ` + Vite project runs, all directories exist per spec section 13, CSS custom properties match spec section 7.2, index.html has 5 screen sections + + + + Task 2: TypeScript interfaces + IndexedDB wrapper + + src/types.ts, src/storage/db.ts, src/storage/db.test.ts + + + SPEC.md (section 6.3 for IndexedDB stores, section 4.1 for level structure, section 5.2 for companion types) + src/types.ts (after Task 1 creates it — may be empty placeholder) + + + - Test: openDB() resolves to an IDBDatabase with 5 object store names + - Test: saveProgress({ currentLevel: 1, ... }) then getProgress() returns same data + - Test: saveSettings({ audioEnabled: true, ... }) then getSettings() returns same data + - Test: getProgress() on empty DB returns null (not crash) + - Test: getSettings() on empty DB returns null (not crash) + + + 1. Create src/types.ts with ALL interfaces per spec section 6.3 (per FNDN-03): + + ```typescript + // Companion types + export type CompanionType = 'fee' | 'einhorn' | 'fuchs' | 'eule'; + export type KeyboardLayout = 'de' | 'ch'; + + // Level status for the forest map + export type LevelStatus = 'locked' | 'current' | 'completed'; + + // Progress store + export interface Progress { + id: 1; // singleton row + currentLevel: number; + completedLevels: number[]; + totalSessions: number; + sessionDates: string[]; // ISO date strings (days only) + selectedCharacter: CompanionType; + selectedLayout: KeyboardLayout; + } + + // Companion assets store + export interface CompanionAssets { + characterType: CompanionType; + characterSheet: Blob; + avatarImage: Blob; + generatedAt: string; // ISO date + } + + // Style reference store + export interface StyleReference { + id: 1; // singleton + imageBlob: Blob; + generatedAt: string; + } + + // Forest element store + export interface ForestElement { + id?: number; // auto-increment + levelCompleted: number; + imageBlob: Blob; + imagePrompt: string; + description: string; + companionText: string; + position: { x: number; y: number }; + createdAt: string; + } + + // Settings store + export interface Settings { + id: 1; // singleton + audioEnabled: boolean; + apiKey: string; + } + + // Level definition (used in game/levels.ts) + export interface Level { + level: number; + newKeys: string[]; + allKeys: string[]; // cumulative keys up to this level + fingerMap: Record; // key -> CSS variable name for finger color + } + + // Typing exercise state + export interface TypingExerciseState { + currentLetterIndex: number; + letters: string[]; + intervalMs: number; + correctStreak: number; + totalCorrect: number; + totalErrors: number; + } + + // Screen names + export type ScreenName = 'welcome' | 'forest' | 'lesson' | 'reward' | 'parent'; + ``` + + 2. Create src/storage/db.ts — IndexedDB wrapper (per D-02, all 5 stores created, only progress + settings fully used): + + ```typescript + import type { Progress, Settings, ForestElement, CompanionAssets, StyleReference } from '../types'; + + const DB_NAME = 'zauberwald'; + const DB_VERSION = 1; + + export function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains('progress')) { + db.createObjectStore('progress', { keyPath: 'id' }); + } + if (!db.objectStoreNames.contains('companionAssets')) { + db.createObjectStore('companionAssets', { keyPath: 'characterType' }); + } + if (!db.objectStoreNames.contains('styleReference')) { + db.createObjectStore('styleReference', { keyPath: 'id' }); + } + if (!db.objectStoreNames.contains('forestElements')) { + db.createObjectStore('forestElements', { keyPath: 'id', autoIncrement: true }); + } + if (!db.objectStoreNames.contains('settings')) { + db.createObjectStore('settings', { keyPath: 'id' }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + export async function getProgress(db: IDBDatabase): Promise { ... } + export async function saveProgress(db: IDBDatabase, progress: Progress): Promise { ... } + export async function getSettings(db: IDBDatabase): Promise { ... } + export async function saveSettings(db: IDBDatabase, settings: Settings): Promise { ... } + ``` + + Each get/save function wraps IDB transaction in a Promise. Use 'readonly' for gets, 'readwrite' for saves. Store name matches interface. + + 3. Create src/storage/db.test.ts using vitest with fake-indexeddb: + - `npm install -D fake-indexeddb` + - Import `import 'fake-indexeddb/auto'` at top of test + - Test all behaviors listed above + + + cd /home/dev/workspace/zauberwald && npx vitest run src/storage/db.test.ts + + + - src/types.ts exports: Progress, ForestElement, CompanionAssets, Settings, StyleReference, Level, CompanionType, KeyboardLayout, LevelStatus, ScreenName, TypingExerciseState + - src/types.ts contains `type CompanionType = 'fee' | 'einhorn' | 'fuchs' | 'eule'` + - src/types.ts contains `type KeyboardLayout = 'de' | 'ch'` + - src/storage/db.ts exports: openDB, getProgress, saveProgress, getSettings, saveSettings + - src/storage/db.ts contains `const DB_NAME = 'zauberwald'` + - src/storage/db.ts contains all 5 store names: 'progress', 'companionAssets', 'styleReference', 'forestElements', 'settings' + - src/storage/db.test.ts exists and `npx vitest run src/storage/db.test.ts` exits 0 + - package.json contains `fake-indexeddb` in devDependencies + + All TypeScript interfaces exported, IndexedDB wrapper creates 5 stores, read/write for progress and settings works, all tests pass + + + + + +- `npm run dev` starts without errors on port 5173 +- `npx vitest run` passes all DB tests +- `npx biome check .` shows no errors (or only warnings) +- All directories from spec section 13 exist +- index.html has 5 screen sections +- CSS custom properties match spec section 7.2 exactly + + + +- Vite dev server accessible at 0.0.0.0:5173 +- TypeScript compiles without errors in strict mode +- IndexedDB wrapper tested and working with 5 stores +- CSS design system complete with all palette colors and typography +- Project structure matches spec section 13 + + + +After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-01-SUMMARY.md` + diff --git a/.planning/phases/01-grundger-st-tippmechanik/01-02-PLAN.md b/.planning/phases/01-grundger-st-tippmechanik/01-02-PLAN.md new file mode 100644 index 0000000..cfb9fc0 --- /dev/null +++ b/.planning/phases/01-grundger-st-tippmechanik/01-02-PLAN.md @@ -0,0 +1,341 @@ +--- +phase: 01-grundger-st-tippmechanik +plan: 02 +type: execute +wave: 2 +depends_on: ["01-01"] +files_modified: + - src/app.ts + - src/ui/screens.ts + - src/companion/characters.ts + - index.html +autonomous: true +requirements: + - FNDN-05 + - ONBD-01 + - ONBD-02 + - ONBD-03 + - ONBD-04 + +must_haves: + truths: + - "App startet und zeigt den Willkommens-Screen" + - "4 Begleitfigur-Karten sind klickbar und zeigen Emoji + Name" + - "DE/CH Layout-Toggle ist waehlbar" + - "Nach Auswahl + Los gehts wechselt die App zur Wald-Uebersicht" + - "Nach Browser-Reload bleibt die Auswahl erhalten und Willkommens-Screen wird uebersprungen" + artifacts: + - path: "src/app.ts" + provides: "Screen management and routing" + exports: ["showScreen", "initApp"] + - path: "src/ui/screens.ts" + provides: "Welcome screen logic and companion selection" + exports: ["initWelcomeScreen"] + - path: "src/companion/characters.ts" + provides: "Companion definitions (name, emoji, personality)" + exports: ["companions", "CompanionDefinition"] + key_links: + - from: "src/app.ts" + to: "src/storage/db.ts" + via: "checks progress on init to skip welcome" + pattern: "getProgress" + - from: "src/ui/screens.ts" + to: "src/storage/db.ts" + via: "saves companion + layout choice" + pattern: "saveProgress" + - from: "src/main.ts" + to: "src/app.ts" + via: "calls initApp() on DOMContentLoaded" + pattern: "initApp" +--- + + +Implement screen management (CSS fade transitions between 5 screens) and the complete onboarding flow: companion selection, layout selection, persistence, and navigation to forest. + +Purpose: The user must be able to start the app, choose a companion and layout, and reach the forest overview. This is the entry point for all subsequent gameplay. +Output: Working welcome-to-forest flow with IndexedDB persistence. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md +@.planning/phases/01-grundger-st-tippmechanik/01-01-SUMMARY.md +@SPEC.md (sections 5.2, 7.1, 7.2) + + +From src/types.ts: +```typescript +export type CompanionType = 'fee' | 'einhorn' | 'fuchs' | 'eule'; +export type KeyboardLayout = 'de' | 'ch'; +export type ScreenName = 'welcome' | 'forest' | 'lesson' | 'reward' | 'parent'; +export interface Progress { + id: 1; + currentLevel: number; + completedLevels: number[]; + totalSessions: number; + sessionDates: string[]; + selectedCharacter: CompanionType; + selectedLayout: KeyboardLayout; +} +``` + +From src/storage/db.ts: +```typescript +export function openDB(): Promise; +export async function getProgress(db: IDBDatabase): Promise; +export async function saveProgress(db: IDBDatabase, progress: Progress): Promise; +``` + + + + + + + Task 1: Screen management + companion definitions + + src/app.ts, src/companion/characters.ts + + + src/types.ts + src/storage/db.ts + src/styles/main.css + index.html + SPEC.md (section 5.2 for companion definitions, section 7.1 for screen list) + + + 1. Create src/companion/characters.ts with companion definitions (per D-05, emoji placeholders): + ```typescript + import type { CompanionType } from '../types'; + + export interface CompanionDefinition { + type: CompanionType; + name: string; + emoji: string; + personality: string; + } + + export const companions: CompanionDefinition[] = [ + { type: 'fee', name: 'Lila', emoji: '🧚', personality: 'Sanft, ermutigend, ein bisschen vertraeumt' }, + { type: 'einhorn', name: 'Stella', emoji: '🦄', personality: 'Froehlich, enthusiastisch, feiert jeden kleinen Erfolg' }, + { type: 'fuchs', name: 'Finn', emoji: '🦊', personality: 'Ruhig, weise, humorvoll' }, + { type: 'eule', name: 'Elsa', emoji: '🦉', personality: 'Geduldig, warm, grossmuetterlich' }, + ]; + ``` + + 2. Create src/app.ts with screen management (per D-01): + ```typescript + import type { ScreenName } from './types'; + import { openDB, getProgress } from './storage/db'; + import { initWelcomeScreen } from './ui/screens'; + + let db: IDBDatabase; + + export function showScreen(name: ScreenName): void { + document.querySelectorAll('.screen').forEach(el => { + el.classList.remove('screen--active'); + }); + const target = document.getElementById(`screen-${name}`); + if (target) { + target.classList.add('screen--active'); + } + } + + export function getDB(): IDBDatabase { + return db; + } + + export async function initApp(): Promise { + db = await openDB(); + const progress = await getProgress(db); + if (progress && progress.selectedCharacter) { + // Returning user — skip welcome, go to forest + showScreen('forest'); + } else { + // First time — show welcome + initWelcomeScreen(db, showScreen); + showScreen('welcome'); + } + } + ``` + + 3. Update src/main.ts to call initApp: + ```typescript + import './styles/main.css'; + import { initApp } from './app'; + document.addEventListener('DOMContentLoaded', initApp); + ``` + + + cd /home/dev/workspace/zauberwald && npx tsc --noEmit + + + - src/companion/characters.ts exports `companions` array with 4 entries (fee, einhorn, fuchs, eule) + - src/companion/characters.ts contains emoji strings: '🧚', '🦄', '🦊', '🦉' + - src/app.ts exports `showScreen` and `initApp` + - src/app.ts imports from `./storage/db` and `./ui/screens` + - src/app.ts contains logic: if progress exists -> showScreen('forest'), else showScreen('welcome') + - src/main.ts contains `import { initApp } from './app'` and `initApp` + - `npx tsc --noEmit` exits 0 + + Screen transitions work via CSS class toggling, companion data defined, app routes to welcome or forest based on saved progress + + + + Task 2: Welcome screen UI + onboarding flow + + src/ui/screens.ts, index.html, src/styles/main.css + + + src/app.ts + src/companion/characters.ts + src/types.ts + src/storage/db.ts + src/styles/main.css + index.html + + + 1. Update index.html — populate the welcome screen section with: + ```html +
+
+

Willkommen im Zauberwald!

+

Waehle deine Begleitfigur:

+
+ +
+

Tastatur-Layout:

+
+ + +
+ +
+
+ ``` + Keep the other 4 screen sections empty (just the section tag). + + 2. Create src/ui/screens.ts: + ```typescript + import type { CompanionType, KeyboardLayout, ScreenName, Progress } from '../types'; + import { companions } from '../companion/characters'; + import { saveProgress } from '../storage/db'; + + export function initWelcomeScreen( + db: IDBDatabase, + navigateTo: (screen: ScreenName) => void + ): void { + let selectedCompanion: CompanionType | null = null; + let selectedLayout: KeyboardLayout = 'de'; + + const grid = document.getElementById('companion-grid')!; + const startBtn = document.getElementById('start-btn') as HTMLButtonElement; + const layoutToggle = document.getElementById('layout-toggle')!; + + // Render companion cards + companions.forEach(c => { + const card = document.createElement('button'); + card.className = 'companion-card'; + card.dataset.companion = c.type; + card.innerHTML = ` + ${c.emoji} + ${c.name} + `; + card.addEventListener('click', () => { + grid.querySelectorAll('.companion-card').forEach(el => + el.classList.remove('companion-card--selected') + ); + card.classList.add('companion-card--selected'); + selectedCompanion = c.type; + startBtn.disabled = false; + }); + grid.appendChild(card); + }); + + // Layout toggle + layoutToggle.addEventListener('click', (e) => { + const btn = (e.target as HTMLElement).closest('.layout-btn') as HTMLButtonElement | null; + if (!btn) return; + layoutToggle.querySelectorAll('.layout-btn').forEach(el => + el.classList.remove('layout-btn--active') + ); + btn.classList.add('layout-btn--active'); + selectedLayout = btn.dataset.layout as KeyboardLayout; + }); + + // Start button + startBtn.addEventListener('click', async () => { + if (!selectedCompanion) return; + const progress: Progress = { + id: 1, + currentLevel: 1, + completedLevels: [], + totalSessions: 0, + sessionDates: [], + selectedCharacter: selectedCompanion, + selectedLayout: selectedLayout, + }; + await saveProgress(db, progress); + navigateTo('forest'); + }); + } + ``` + + 3. Add CSS for welcome screen in src/styles/main.css: + - `.welcome` — centered flex column, padding 2rem, max-width 600px, margin auto + - `.welcome h1` — font-family Quicksand, font-size 2rem, color var(--text-dark) + - `.welcome__companions` / `#companion-grid` — display grid, grid-template-columns repeat(2, 1fr), gap 1rem + - `.companion-card` — background white, border 3px solid transparent, border-radius 16px, padding 1.5rem, cursor pointer, transition border-color 0.2s, text-align center, display flex, flex-direction column, align-items center, gap 0.5rem + - `.companion-card:hover` — border-color var(--accent-gold) + - `.companion-card--selected` — border-color var(--accent-green), background var(--bg-forest) + - `.companion-card__emoji` — font-size 3rem + - `.companion-card__name` — font-family Quicksand, font-weight 600, font-size 1.2rem, color var(--text-dark) + - `.layout-btn` — padding 0.5rem 1.5rem, border-radius 8px, border 2px solid var(--text-light), background white, cursor pointer, font-family Nunito, font-size 1rem + - `.layout-btn--active` — background var(--accent-green), color white, border-color var(--accent-green) + - `.welcome__start-btn` — margin-top 1.5rem, padding 1rem 3rem, border-radius 12px, background var(--accent-green), color white, border none, font-family Quicksand, font-weight 700, font-size 1.3rem, cursor pointer, opacity 1, transition opacity 0.2s + - `.welcome__start-btn:disabled` — opacity 0.4, cursor not-allowed + - `.welcome__layout-toggle` — display flex, gap 0.5rem, margin-top 0.5rem +
+ + cd /home/dev/workspace/zauberwald && npx tsc --noEmit && npm run dev -- --strictPort 2>&1 & sleep 3 && curl -s http://localhost:5173 | grep -q "companion-grid" && kill %1 && echo "PASS" + + + - index.html contains `id="companion-grid"` and `id="start-btn"` and `id="layout-toggle"` + - index.html contains `data-layout="de"` and `data-layout="ch"` + - src/ui/screens.ts exports `initWelcomeScreen` + - src/ui/screens.ts contains `saveProgress(db, progress)` call + - src/ui/screens.ts contains `navigateTo('forest')` after save + - src/ui/screens.ts creates 4 companion cards with `.companion-card` class + - src/styles/main.css contains `.companion-card`, `.companion-card--selected`, `.welcome__start-btn` + - src/styles/main.css contains `.layout-btn--active` + - `npx tsc --noEmit` exits 0 + - Welcome screen renders 4 companion cards and DE/CH toggle in browser + + Welcome screen shows 4 emoji companion cards, DE/CH layout toggle, and "Los geht's" button. Selecting companion enables start. Clicking start saves to IndexedDB and navigates to forest screen. On reload, app skips welcome and shows forest directly. +
+ +
+ + +- App loads and shows welcome screen with 4 companion cards +- Clicking a companion highlights it and enables "Los geht's" +- DE/CH toggle switches active state +- Clicking "Los geht's" transitions to forest screen (empty for now) +- Refreshing the page skips welcome and shows forest directly +- Clearing IndexedDB and refreshing shows welcome again + + + +- Complete onboarding flow: select companion + layout, start, persist, skip on return +- Screen transitions use CSS fade (opacity transition) +- All companion data matches spec section 5.2 names + + + +After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-02-SUMMARY.md` + diff --git a/.planning/phases/01-grundger-st-tippmechanik/01-03-PLAN.md b/.planning/phases/01-grundger-st-tippmechanik/01-03-PLAN.md new file mode 100644 index 0000000..7690d25 --- /dev/null +++ b/.planning/phases/01-grundger-st-tippmechanik/01-03-PLAN.md @@ -0,0 +1,407 @@ +--- +phase: 01-grundger-st-tippmechanik +plan: 03 +type: execute +wave: 2 +depends_on: ["01-01"] +files_modified: + - src/game/levels.ts + - src/game/keyboard.ts + - src/game/levels.test.ts + - src/styles/main.css +autonomous: true +requirements: + - KYBD-01 + - KYBD-02 + - KYBD-03 + - KYBD-04 + - KYBD-05 + - LEVL-01 + +must_haves: + truths: + - "Bildschirmtastatur zeigt vollstaendiges QWERTZ-Layout mit allen Tasten" + - "Gelernte Tasten leuchten in ihrer Fingerfarbe, ungelernte sind grau" + - "Eine aktive Taste pulsiert sanft (CSS-Animation)" + - "Tastendruck zeigt press-Animation (scale down)" + - "Stufen 1-6 sind als Daten definiert mit korrekten Tastenzuordnungen" + - "Beide Layouts DE/CH sind als Daten hinterlegt" + artifacts: + - path: "src/game/levels.ts" + provides: "Level definitions 1-6 with key sets and finger mappings" + exports: ["levels", "getLevelByNumber", "getKeysUpToLevel"] + - path: "src/game/keyboard.ts" + provides: "QWERTZ keyboard renderer with finger colors" + exports: ["renderKeyboard", "highlightKey", "pressKey", "updateKeyboardForLevel"] + key_links: + - from: "src/game/keyboard.ts" + to: "src/game/levels.ts" + via: "reads level data to determine which keys are active" + pattern: "import.*levels" + - from: "src/game/keyboard.ts" + to: "src/styles/main.css" + via: "uses finger color CSS variables" + pattern: "--finger-" +--- + + +Create the QWERTZ keyboard component with finger-colored keys and the level definitions (stages 1-6). The keyboard is the primary visual feedback during typing exercises. + +Purpose: The keyboard component is needed by the typing exercise (Plan 04) and the forest overview (Plan 05). Level definitions drive all gameplay. +Output: Renderable keyboard with finger colors, pulse/press animations, and complete level 1-6 data. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md +@.planning/phases/01-grundger-st-tippmechanik/01-01-SUMMARY.md +@SPEC.md (sections 4.1, 8.1, 8.2, 8.3) + + +From src/types.ts: +```typescript +export type KeyboardLayout = 'de' | 'ch'; + +export interface Level { + level: number; + newKeys: string[]; + allKeys: string[]; + fingerMap: Record; +} +``` + + + + + + + Task 1: Level definitions with key sets and finger mappings + + src/game/levels.ts, src/game/levels.test.ts + + + src/types.ts + SPEC.md (section 4.1 for progression table, section 8.3 for finger assignments) + + + - Test: levels array has exactly 6 entries + - Test: Level 1 newKeys = ['f', 'j', ' '] (F, J, Space) + - Test: Level 2 newKeys = ['d', 'k'] + - Test: Level 3 newKeys = ['s', 'l'] + - Test: Level 4 newKeys = ['a', 'ö'] + - Test: Level 5 newKeys = ['g', 'h'] + - Test: Level 6 newKeys = ['e', 'i'] + - Test: Level 3 allKeys includes all keys from levels 1-3 + - Test: getLevelByNumber(1) returns level 1 object + - Test: getKeysUpToLevel(3) returns cumulative keys for levels 1-3 + - Test: Each key in allKeys has a fingerMap entry + + + Create src/game/levels.ts with level definitions per spec section 4.1 and finger assignments per section 8.3: + + ```typescript + import type { Level } from '../types'; + + // Finger color CSS variable mapping per spec section 8.3 + // Left hand: pinky=Q/A/Y/1, ring=W/S/X/2, mid=E/D/C/3, index=R/F/V/T/G/B/4/5 + // Right hand: index=U/J/M/Z/H/N/6/7, mid=I/K/,/8, ring=O/L/./9, pinky=P/Ö/Ä/Ü/0/ß + // Thumb: Space + + const fingerMap: Record = { + // Left pinky + 'q': '--finger-l-pinky', 'a': '--finger-l-pinky', 'y': '--finger-l-pinky', + // Left ring + 'w': '--finger-l-ring', 's': '--finger-l-ring', 'x': '--finger-l-ring', + // Left middle + 'e': '--finger-l-mid', 'd': '--finger-l-mid', 'c': '--finger-l-mid', + // Left index + 'r': '--finger-l-index', 'f': '--finger-l-index', 'v': '--finger-l-index', + 't': '--finger-l-index', 'g': '--finger-l-index', 'b': '--finger-l-index', + // Right index + 'u': '--finger-r-index', 'j': '--finger-r-index', 'm': '--finger-r-index', + 'z': '--finger-r-index', 'h': '--finger-r-index', 'n': '--finger-r-index', + // Right middle + 'i': '--finger-r-mid', 'k': '--finger-r-mid', + // Right ring + 'o': '--finger-r-ring', 'l': '--finger-r-ring', + // Right pinky + 'p': '--finger-r-pinky', 'ö': '--finger-r-pinky', 'ä': '--finger-r-pinky', 'ü': '--finger-r-pinky', + // Thumb + ' ': '--finger-thumb', + }; + + export const levels: Level[] = [ + { level: 1, newKeys: ['f', 'j', ' '], allKeys: ['f', 'j', ' '], fingerMap }, + { level: 2, newKeys: ['d', 'k'], allKeys: ['f', 'j', ' ', 'd', 'k'], fingerMap }, + { level: 3, newKeys: ['s', 'l'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l'], fingerMap }, + { level: 4, newKeys: ['a', 'ö'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö'], fingerMap }, + { level: 5, newKeys: ['g', 'h'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö', 'g', 'h'], fingerMap }, + { level: 6, newKeys: ['e', 'i'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö', 'g', 'h', 'e', 'i'], fingerMap }, + ]; + + export function getLevelByNumber(n: number): Level | undefined { + return levels.find(l => l.level === n); + } + + export function getKeysUpToLevel(n: number): string[] { + const level = getLevelByNumber(n); + return level ? level.allKeys : []; + } + ``` + + Create src/game/levels.test.ts testing all behaviors listed above. + + + cd /home/dev/workspace/zauberwald && npx vitest run src/game/levels.test.ts + + + - src/game/levels.ts exports `levels` (array of 6), `getLevelByNumber`, `getKeysUpToLevel` + - levels[0].newKeys deep equals ['f', 'j', ' '] + - levels[5].newKeys deep equals ['e', 'i'] + - levels[5].allKeys contains 13 keys (f,j,space,d,k,s,l,a,ö,g,h,e,i) + - fingerMap maps 'f' to '--finger-l-index' and 'j' to '--finger-r-index' + - fingerMap maps ' ' to '--finger-thumb' + - `npx vitest run src/game/levels.test.ts` exits 0 + + Levels 1-6 defined with correct key sets per spec progression table, finger mapping matches spec section 8.3, all tests pass + + + + Task 2: QWERTZ keyboard renderer with finger colors and animations + + src/game/keyboard.ts, src/styles/main.css + + + src/game/levels.ts + src/types.ts + src/styles/main.css + SPEC.md (section 8.1 for keyboard layout, section 8.2 for DE/CH differences) + + + 1. Create src/game/keyboard.ts: + + ```typescript + import type { KeyboardLayout } from '../types'; + import { getLevelByNumber } from './levels'; + + // QWERTZ rows — each row is an array of key labels + // For DE and CH layouts (identical for letters, differ on special chars above level 14) + const KEYBOARD_ROWS_DE = [ + ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß'], + ['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü'], + ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'], + ['y', 'x', 'c', 'v', 'b', 'n', 'm'], + [' '], // Spacebar + ]; + + const KEYBOARD_ROWS_CH = [ + ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], + ['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü'], + ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'], + ['y', 'x', 'c', 'v', 'b', 'n', 'm'], + [' '], + ]; + + // Finger color mapping (shared with levels.ts fingerMap) + const fingerColorMap: Record = { + 'q': '--finger-l-pinky', 'a': '--finger-l-pinky', 'y': '--finger-l-pinky', '1': '--finger-l-pinky', + 'w': '--finger-l-ring', 's': '--finger-l-ring', 'x': '--finger-l-ring', '2': '--finger-l-ring', + 'e': '--finger-l-mid', 'd': '--finger-l-mid', 'c': '--finger-l-mid', '3': '--finger-l-mid', + 'r': '--finger-l-index', 'f': '--finger-l-index', 'v': '--finger-l-index', '4': '--finger-l-index', + 't': '--finger-l-index', 'g': '--finger-l-index', 'b': '--finger-l-index', '5': '--finger-l-index', + 'z': '--finger-r-index', 'h': '--finger-r-index', 'n': '--finger-r-index', '6': '--finger-r-index', + 'u': '--finger-r-index', 'j': '--finger-r-index', 'm': '--finger-r-index', '7': '--finger-r-index', + 'i': '--finger-r-mid', 'k': '--finger-r-mid', '8': '--finger-r-mid', + 'o': '--finger-r-ring', 'l': '--finger-r-ring', '9': '--finger-r-ring', + 'p': '--finger-r-pinky', 'ö': '--finger-r-pinky', 'ä': '--finger-r-pinky', + 'ü': '--finger-r-pinky', '0': '--finger-r-pinky', 'ß': '--finger-r-pinky', + ' ': '--finger-thumb', + }; + + let keyboardContainer: HTMLElement | null = null; + let keyElements: Map = new Map(); + + export function renderKeyboard( + container: HTMLElement, + layout: KeyboardLayout, + currentLevel: number + ): void { + keyboardContainer = container; + keyElements.clear(); + container.innerHTML = ''; + container.className = 'keyboard'; + + const rows = layout === 'ch' ? KEYBOARD_ROWS_CH : KEYBOARD_ROWS_DE; + const level = getLevelByNumber(currentLevel); + const activeKeys = level ? level.allKeys : []; + + rows.forEach((row, rowIndex) => { + const rowEl = document.createElement('div'); + rowEl.className = `keyboard__row keyboard__row--${rowIndex}`; + + row.forEach(key => { + const keyEl = document.createElement('div'); + const isActive = activeKeys.includes(key); + const colorVar = fingerColorMap[key]; + + keyEl.className = 'keyboard__key'; + if (key === ' ') keyEl.classList.add('keyboard__key--space'); + if (isActive && colorVar) { + keyEl.style.backgroundColor = `var(${colorVar})`; + keyEl.classList.add('keyboard__key--active'); + } else { + keyEl.classList.add('keyboard__key--inactive'); + } + + keyEl.textContent = key === ' ' ? '' : key.toUpperCase(); + keyEl.dataset.key = key; + keyElements.set(key, keyEl); + rowEl.appendChild(keyEl); + }); + + container.appendChild(rowEl); + }); + } + + export function highlightKey(key: string): void { + // Remove previous highlights + keyElements.forEach(el => el.classList.remove('keyboard__key--highlight')); + const el = keyElements.get(key.toLowerCase()); + if (el) el.classList.add('keyboard__key--highlight'); + } + + export function pressKey(key: string): void { + const el = keyElements.get(key.toLowerCase()); + if (el) { + el.classList.add('keyboard__key--pressed'); + setTimeout(() => el.classList.remove('keyboard__key--pressed'), 150); + } + } + + export function updateKeyboardForLevel(currentLevel: number): void { + const level = getLevelByNumber(currentLevel); + const activeKeys = level ? level.allKeys : []; + keyElements.forEach((el, key) => { + const colorVar = fingerColorMap[key]; + if (activeKeys.includes(key) && colorVar) { + el.style.backgroundColor = `var(${colorVar})`; + el.classList.remove('keyboard__key--inactive'); + el.classList.add('keyboard__key--active'); + } else { + el.style.backgroundColor = ''; + el.classList.remove('keyboard__key--active'); + el.classList.add('keyboard__key--inactive'); + } + }); + } + ``` + + 2. Add keyboard CSS to src/styles/main.css: + + ```css + /* Keyboard */ + .keyboard { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 1rem; + background: rgba(255, 255, 255, 0.6); + border-radius: 12px; + user-select: none; + } + + .keyboard__row { + display: flex; + gap: 4px; + } + + .keyboard__key { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 8px; + font-family: 'Quicksand', sans-serif; + font-weight: 700; + font-size: 1rem; + color: var(--text-dark); + background: #e0e0e0; + transition: transform 0.1s ease, background-color 0.2s ease, box-shadow 0.2s ease; + cursor: default; + } + + .keyboard__key--space { + width: 280px; + } + + .keyboard__key--inactive { + background: #e0e0e0; + color: var(--text-light); + opacity: 0.5; + } + + .keyboard__key--active { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + /* Pulse animation for the target key (KYBD-03) */ + .keyboard__key--highlight { + animation: key-pulse 1.2s ease-in-out infinite; + box-shadow: 0 0 12px rgba(232, 184, 75, 0.6); + } + + @keyframes key-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.08); } + } + + /* Press animation (KYBD-03) */ + .keyboard__key--pressed { + transform: scale(0.9) !important; + animation: none; + } + ``` + + + cd /home/dev/workspace/zauberwald && npx tsc --noEmit && echo "PASS" + + + - src/game/keyboard.ts exports `renderKeyboard`, `highlightKey`, `pressKey`, `updateKeyboardForLevel` + - src/game/keyboard.ts contains KEYBOARD_ROWS_DE with 5 rows (numbers, top, home, bottom, space) + - src/game/keyboard.ts contains KEYBOARD_ROWS_CH with 5 rows + - src/game/keyboard.ts contains fingerColorMap with entries for 'f' -> '--finger-l-index', 'j' -> '--finger-r-index', ' ' -> '--finger-thumb' + - src/styles/main.css contains `.keyboard__key--highlight` with `animation: key-pulse` + - src/styles/main.css contains `@keyframes key-pulse` + - src/styles/main.css contains `.keyboard__key--pressed` with `transform: scale(0.9)` + - src/styles/main.css contains `.keyboard__key--inactive` with `opacity: 0.5` + - `npx tsc --noEmit` exits 0 + + Full QWERTZ keyboard renders with finger colors for active keys, grayed inactive keys, pulse animation on target key, press animation on keydown. Both DE and CH layouts supported. + + + + + +- `npx vitest run src/game/levels.test.ts` passes +- `npx tsc --noEmit` passes +- Keyboard renders correctly when manually instantiated (verified in Plan 05 integration) +- Level data is correct per spec progression table + + + +- Levels 1-6 defined with correct key progressions matching spec section 4.1 +- Keyboard renders full QWERTZ layout with finger colors from spec section 8.3 +- Pulse and press animations work via CSS classes +- Both DE and CH layout data exist + + + +After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-03-SUMMARY.md` + diff --git a/.planning/phases/01-grundger-st-tippmechanik/01-04-PLAN.md b/.planning/phases/01-grundger-st-tippmechanik/01-04-PLAN.md new file mode 100644 index 0000000..1bf0c8e --- /dev/null +++ b/.planning/phases/01-grundger-st-tippmechanik/01-04-PLAN.md @@ -0,0 +1,447 @@ +--- +phase: 01-grundger-st-tippmechanik +plan: 04 +type: tdd +wave: 3 +depends_on: ["01-02", "01-03"] +files_modified: + - src/game/typing.ts + - src/game/typing.test.ts + - src/styles/main.css +autonomous: true +requirements: + - TYPE-01 + - TYPE-02 + - TYPE-03 + - TYPE-04 + - LEVL-02 + - LEVL-03 + +must_haves: + truths: + - "Buchstaben erscheinen einzeln auf dem Bildschirm" + - "Richtiger Tastendruck loest den Buchstaben auf" + - "Falscher Tastendruck tut nichts, richtige Taste blinkt auf Tastatur" + - "Tempo passt sich an: schneller nach 2x richtig, langsamer nach Fehler" + - "Nach 8-12 richtigen Buchstaben ist die Uebung abgeschlossen" + - "Abgeschlossene Stufe schaltet naechste frei" + artifacts: + - path: "src/game/typing.ts" + provides: "Core typing exercise engine" + exports: ["createTypingExercise", "TypingExercise"] + - path: "src/game/typing.test.ts" + provides: "TDD tests for typing logic" + min_lines: 50 + key_links: + - from: "src/game/typing.ts" + to: "src/game/levels.ts" + via: "gets key set for current level" + pattern: "getKeysUpToLevel|getLevelByNumber" + - from: "src/game/typing.ts" + to: "src/game/keyboard.ts" + via: "highlights target key, shows press animation" + pattern: "highlightKey|pressKey" + - from: "src/game/typing.ts" + to: "src/storage/db.ts" + via: "saves progress after lesson completion" + pattern: "saveProgress" +--- + + +Implement the core typing exercise engine using TDD. This is the heart of the app: letters appear, the child types them, adaptive tempo adjusts, and the lesson completes after 8-12 correct keystrokes. + +Purpose: The typing exercise is the core gameplay loop. Getting the logic right (adaptive tempo, correct/wrong handling, completion) is critical and benefits from TDD. +Output: Tested typing engine that manages letter generation, input handling, tempo adaptation, and lesson completion. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md +@.planning/phases/01-grundger-st-tippmechanik/01-01-SUMMARY.md +@.planning/phases/01-grundger-st-tippmechanik/01-03-SUMMARY.md +@SPEC.md (section 3.1 phase 2 "Ueben", section 6.2 adaptive tempo) + + +From src/types.ts: +```typescript +export interface TypingExerciseState { + currentLetterIndex: number; + letters: string[]; + intervalMs: number; + correctStreak: number; + totalCorrect: number; + totalErrors: number; +} + +export interface Progress { + id: 1; + currentLevel: number; + completedLevels: number[]; + totalSessions: number; + sessionDates: string[]; + selectedCharacter: CompanionType; + selectedLayout: KeyboardLayout; +} +``` + +From src/game/levels.ts: +```typescript +export function getLevelByNumber(n: number): Level | undefined; +export function getKeysUpToLevel(n: number): string[]; +``` + +From src/game/keyboard.ts: +```typescript +export function highlightKey(key: string): void; +export function pressKey(key: string): void; +``` + + + + + + + Task 1: Typing exercise logic (RED-GREEN-REFACTOR) + + src/game/typing.ts, src/game/typing.test.ts + + + src/types.ts + src/game/levels.ts + src/game/keyboard.ts + SPEC.md (section 3.1 for exercise structure, section 6.2 for adaptive tempo rules) + .planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md (D-03, D-04, D-08 for exercise scope and tempo rules) + + + - Test: generateLetters(level=1) returns 8-12 letters, all from ['f', 'j', ' '] + - Test: generateLetters(level=3) returns letters only from levels 1-3 key set + - Test: Initial interval is 3000ms + - Test: After 2 correct in sequence, interval decreases by 200ms (3000 -> 2800) + - Test: After 1 error, interval increases by 500ms (2800 -> 3300) + - Test: Interval never goes below 1500ms + - Test: Interval never goes above 4000ms + - Test: handleKeyPress with correct key increments currentLetterIndex and totalCorrect + - Test: handleKeyPress with wrong key does NOT increment currentLetterIndex, increments totalErrors + - Test: Exercise is complete when currentLetterIndex equals letters.length + - Test: correctStreak resets to 0 on error + - Test: correctStreak increments on correct key + + + **RED phase:** Create src/game/typing.test.ts with all tests above. Tests import from typing.ts which does not exist yet. Run tests — they must fail. + + **GREEN phase:** Create src/game/typing.ts implementing: + + ```typescript + import type { TypingExerciseState } from '../types'; + import { getKeysUpToLevel } from './levels'; + + const MIN_LETTERS = 8; + const MAX_LETTERS = 12; + const INITIAL_INTERVAL = 3000; + const INTERVAL_DECREASE = 200; + const INTERVAL_INCREASE = 500; + const MIN_INTERVAL = 1500; + const MAX_INTERVAL = 4000; + const STREAK_THRESHOLD = 2; + + export function generateLetters(level: number): string[] { + const keys = getKeysUpToLevel(level); + // Filter out space for letter-only exercise (space can appear but less frequently) + const letterKeys = keys.filter(k => k !== ' '); + const count = MIN_LETTERS + Math.floor(Math.random() * (MAX_LETTERS - MIN_LETTERS + 1)); + const letters: string[] = []; + for (let i = 0; i < count; i++) { + // 80% letters, 20% chance of space if space is in key set + if (keys.includes(' ') && Math.random() < 0.2) { + letters.push(' '); + } else { + letters.push(letterKeys[Math.floor(Math.random() * letterKeys.length)]); + } + } + return letters; + } + + export function createExerciseState(level: number): TypingExerciseState { + return { + currentLetterIndex: 0, + letters: generateLetters(level), + intervalMs: INITIAL_INTERVAL, + correctStreak: 0, + totalCorrect: 0, + totalErrors: 0, + }; + } + + export interface KeyPressResult { + correct: boolean; + exerciseComplete: boolean; + newInterval: number; + } + + export function handleKeyPress( + state: TypingExerciseState, + pressedKey: string + ): KeyPressResult { + const expectedKey = state.letters[state.currentLetterIndex]; + const correct = pressedKey.toLowerCase() === expectedKey.toLowerCase(); + + if (correct) { + state.currentLetterIndex++; + state.totalCorrect++; + state.correctStreak++; + + // Adaptive tempo: after STREAK_THRESHOLD correct in sequence, speed up + if (state.correctStreak >= STREAK_THRESHOLD) { + state.intervalMs = Math.max(MIN_INTERVAL, state.intervalMs - INTERVAL_DECREASE); + state.correctStreak = 0; // Reset streak after speed adjustment + } + } else { + state.totalErrors++; + state.correctStreak = 0; + // Slow down after error + state.intervalMs = Math.min(MAX_INTERVAL, state.intervalMs + INTERVAL_INCREASE); + } + + return { + correct, + exerciseComplete: state.currentLetterIndex >= state.letters.length, + newInterval: state.intervalMs, + }; + } + + export function getCurrentLetter(state: TypingExerciseState): string | null { + if (state.currentLetterIndex >= state.letters.length) return null; + return state.letters[state.currentLetterIndex]; + } + + export function isExerciseComplete(state: TypingExerciseState): boolean { + return state.currentLetterIndex >= state.letters.length; + } + ``` + + **REFACTOR phase:** Clean up if needed, ensure all exports are clear and constants are well-named. + + Note on D-08: Interval starts at 3000ms, -200ms after 2 consecutive correct (min 1500ms), +500ms after 1 error (max 4000ms). Reset to 3000ms at start of each new lesson (handled by createExerciseState). + + + cd /home/dev/workspace/zauberwald && npx vitest run src/game/typing.test.ts + + + - src/game/typing.ts exports: generateLetters, createExerciseState, handleKeyPress, getCurrentLetter, isExerciseComplete + - src/game/typing.ts contains `INITIAL_INTERVAL = 3000` + - src/game/typing.ts contains `MIN_INTERVAL = 1500` + - src/game/typing.ts contains `MAX_INTERVAL = 4000` + - src/game/typing.ts contains `INTERVAL_DECREASE = 200` + - src/game/typing.ts contains `INTERVAL_INCREASE = 500` + - src/game/typing.test.ts contains at least 10 test cases + - `npx vitest run src/game/typing.test.ts` exits 0 + - handleKeyPress with correct key returns { correct: true } + - handleKeyPress with wrong key returns { correct: false } and does not advance index + + Typing exercise engine tested and working: generates random letters from level key set, handles correct/wrong input, adapts tempo per D-08 rules, detects exercise completion + + + + Task 2: Lesson screen UI wiring (typing exercise + keyboard + falling letters) + + src/ui/screens.ts, index.html, src/styles/main.css + + + src/ui/screens.ts + src/game/typing.ts + src/game/keyboard.ts + src/game/levels.ts + src/types.ts + src/app.ts + src/storage/db.ts + index.html + src/styles/main.css + + + 1. Update index.html — populate the lesson screen section: + ```html +
+
+
+ +
+
+ +
+
+
+ ``` + + 2. Add to src/ui/screens.ts — export a new function `initLessonScreen`: + + ```typescript + import { createExerciseState, handleKeyPress, getCurrentLetter, isExerciseComplete } from '../game/typing'; + import { renderKeyboard, highlightKey, pressKey } from '../game/keyboard'; + import { getProgress, saveProgress } from '../storage/db'; + import type { Progress, TypingExerciseState, KeyboardLayout } from '../types'; + + export function initLessonScreen( + db: IDBDatabase, + level: number, + layout: KeyboardLayout, + onComplete: (level: number) => void + ): () => void { + // Returns a cleanup function + + const letterArea = document.getElementById('letter-area')!; + const keyboardContainer = document.getElementById('lesson-keyboard')!; + const state = createExerciseState(level); + + // Render keyboard + renderKeyboard(keyboardContainer, layout, level); + + // Show first letter + function showCurrentLetter(): void { + const letter = getCurrentLetter(state); + if (!letter) return; + letterArea.innerHTML = ''; + const letterEl = document.createElement('div'); + letterEl.className = 'falling-letter'; + letterEl.textContent = letter === ' ' ? '␣' : letter.toUpperCase(); + letterArea.appendChild(letterEl); + highlightKey(letter); + } + + showCurrentLetter(); + + // Keyboard handler + function onKeyDown(e: KeyboardEvent): void { + // Ignore modifier keys, function keys, etc. + if (e.key.length > 1 && e.key !== ' ') return; + e.preventDefault(); + + const result = handleKeyPress(state, e.key); + pressKey(e.key); + + if (result.correct) { + // Correct: dissolve letter with stardust animation + const currentEl = letterArea.querySelector('.falling-letter'); + if (currentEl) { + currentEl.classList.add('falling-letter--dissolve'); + } + + if (result.exerciseComplete) { + // Lesson complete + document.removeEventListener('keydown', onKeyDown); + onComplete(level); + } else { + // Show next letter after brief delay + setTimeout(() => showCurrentLetter(), 300); + } + } else { + // Wrong: do nothing to letter (per D-03), keyboard already highlights correct key via highlightKey + // The correct key is already highlighted from showCurrentLetter + } + } + + document.addEventListener('keydown', onKeyDown); + + // Return cleanup function + return () => { + document.removeEventListener('keydown', onKeyDown); + letterArea.innerHTML = ''; + }; + } + ``` + + 3. Update src/app.ts to handle lesson start and completion: + - Add a `startLesson(level: number)` function that calls showScreen('lesson') then initLessonScreen + - The `onComplete` callback should: + a. Update progress: add level to completedLevels if not already there, increment totalSessions, add today's date to sessionDates if not already there, set currentLevel to level+1 if level was the current level (per LEVL-02, LEVL-03) + b. Save progress to IndexedDB + c. Navigate to forest screen (per D-07: skip reward in Phase 1, or show placeholder) + + 4. Add CSS for falling letter and dissolve animation to src/styles/main.css: + ```css + .lesson { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2rem; + padding: 2rem; + min-height: 100vh; + background: var(--bg-cream); + } + + .lesson__letter-area { + display: flex; + align-items: center; + justify-content: center; + width: 200px; + height: 200px; + } + + .falling-letter { + font-family: 'Quicksand', sans-serif; + font-size: 5rem; + font-weight: 700; + color: var(--accent-green); + animation: letter-float 2s ease-in-out infinite; + } + + @keyframes letter-float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } + } + + .falling-letter--dissolve { + animation: letter-dissolve 0.3s ease-out forwards; + } + + @keyframes letter-dissolve { + 0% { transform: scale(1); opacity: 1; } + 100% { transform: scale(1.5); opacity: 0; } + } + ``` +
+ + cd /home/dev/workspace/zauberwald && npx tsc --noEmit && npx vitest run + + + - src/ui/screens.ts exports `initLessonScreen` + - src/ui/screens.ts contains `handleKeyPress(state, e.key)` call + - src/ui/screens.ts contains `renderKeyboard(keyboardContainer, layout, level)` call + - src/ui/screens.ts contains `highlightKey(letter)` call in showCurrentLetter + - src/ui/screens.ts contains `pressKey(e.key)` call in onKeyDown + - src/ui/screens.ts contains `onComplete(level)` call when exercise is complete + - src/app.ts contains function that updates completedLevels and saves progress on lesson complete + - index.html contains `id="letter-area"` and `id="lesson-keyboard"` + - src/styles/main.css contains `.falling-letter` with `font-size: 5rem` + - src/styles/main.css contains `@keyframes letter-dissolve` + - src/styles/main.css contains `.falling-letter--dissolve` + - `npx tsc --noEmit` exits 0 + - `npx vitest run` exits 0 (all existing tests still pass) + + Lesson screen shows letters one at a time, keyboard highlights target key, correct press dissolves letter and advances, wrong press does nothing, lesson completes after all letters typed, progress saved with level completion and next level unlocked +
+ +
+ + +- `npx vitest run` passes all tests (typing engine + DB) +- `npx tsc --noEmit` passes +- In browser: can start a lesson, type letters, see keyboard highlights, complete lesson +- After lesson: progress updated, next level unlocked + + + +- Typing exercise engine fully tested with TDD (RED-GREEN-REFACTOR) +- Adaptive tempo works: 3s start, -200ms after 2x correct, +500ms after error, bounds 1.5s-4s +- Lesson screen renders falling letter + keyboard, handles input correctly +- Lesson completion updates progress and unlocks next level + + + +After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-04-SUMMARY.md` + diff --git a/.planning/phases/01-grundger-st-tippmechanik/01-05-PLAN.md b/.planning/phases/01-grundger-st-tippmechanik/01-05-PLAN.md new file mode 100644 index 0000000..1df4aa9 --- /dev/null +++ b/.planning/phases/01-grundger-st-tippmechanik/01-05-PLAN.md @@ -0,0 +1,371 @@ +--- +phase: 01-grundger-st-tippmechanik +plan: 05 +type: execute +wave: 4 +depends_on: ["01-02", "01-04"] +files_modified: + - src/ui/screens.ts + - src/forest/scene.ts + - index.html + - src/styles/main.css + - src/app.ts +autonomous: false +requirements: + - FRST-04 + - FRST-05 + +must_haves: + truths: + - "Wald-Uebersicht zeigt Stufenkarte mit Status locked/current/completed" + - "Weiter-ueben-Button startet die naechste offene Stufe" + - "Nach Lesson-Abschluss kehrt die App zur Wald-Uebersicht zurueck und zeigt aktualisierten Status" + - "Gesamter Flow funktioniert: Welcome -> Forest -> Lesson -> Forest (mit Fortschritt)" + artifacts: + - path: "src/forest/scene.ts" + provides: "Forest overview with level map" + exports: ["initForestScreen", "renderLevelMap"] + key_links: + - from: "src/forest/scene.ts" + to: "src/storage/db.ts" + via: "loads progress to determine level statuses" + pattern: "getProgress" + - from: "src/forest/scene.ts" + to: "src/game/levels.ts" + via: "reads level definitions for the map" + pattern: "import.*levels" + - from: "src/forest/scene.ts" + to: "src/app.ts" + via: "calls startLesson when user clicks a level or continue button" + pattern: "startLesson|navigateTo" +--- + + +Build the forest overview screen with the level map (Stufenkarte) showing locked/current/completed states, and wire together the complete app flow: welcome -> forest -> lesson -> forest. + +Purpose: This is the final integration plan that connects all pieces into a playable app. The user can see their progress and start lessons from the forest screen. +Output: Complete playable Phase 1 app with the core loop working end-to-end. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md +@.planning/phases/01-grundger-st-tippmechanik/01-01-SUMMARY.md +@.planning/phases/01-grundger-st-tippmechanik/01-02-SUMMARY.md +@.planning/phases/01-grundger-st-tippmechanik/01-03-SUMMARY.md +@.planning/phases/01-grundger-st-tippmechanik/01-04-SUMMARY.md +@SPEC.md (section 7.1 for screen structure) + + +From src/types.ts: +```typescript +export type LevelStatus = 'locked' | 'current' | 'completed'; +export type ScreenName = 'welcome' | 'forest' | 'lesson' | 'reward' | 'parent'; +export interface Progress { + id: 1; + currentLevel: number; + completedLevels: number[]; + totalSessions: number; + sessionDates: string[]; + selectedCharacter: CompanionType; + selectedLayout: KeyboardLayout; +} +``` + +From src/game/levels.ts: +```typescript +export const levels: Level[]; +``` + +From src/app.ts: +```typescript +export function showScreen(name: ScreenName): void; +export function getDB(): IDBDatabase; +``` + +From src/storage/db.ts: +```typescript +export async function getProgress(db: IDBDatabase): Promise; +``` + + + + + + + Task 1: Forest screen with level map and continue button + + src/forest/scene.ts, index.html, src/styles/main.css + + + src/types.ts + src/game/levels.ts + src/storage/db.ts + src/app.ts + src/ui/screens.ts + index.html + src/styles/main.css + + + 1. Update index.html — populate forest screen: + ```html +
+
+

Dein Zauberwald

+
+ +
+ +
+
+ ``` + + 2. Create src/forest/scene.ts: + ```typescript + import type { Progress, LevelStatus, ScreenName } from '../types'; + import { levels } from '../game/levels'; + import { getProgress } from '../storage/db'; + + export function getLevelStatus(levelNum: number, progress: Progress): LevelStatus { + if (progress.completedLevels.includes(levelNum)) return 'completed'; + if (levelNum === progress.currentLevel) return 'current'; + return 'locked'; + } + + export function renderLevelMap( + container: HTMLElement, + progress: Progress, + onLevelClick: (level: number) => void + ): void { + container.innerHTML = ''; + + levels.forEach(level => { + const status = getLevelStatus(level.level, progress); + const card = document.createElement('button'); + card.className = `level-card level-card--${status}`; + card.dataset.level = String(level.level); + + const statusEmoji = status === 'completed' ? '✓' : status === 'current' ? '→' : '🔒'; + const keysDisplay = level.newKeys + .filter(k => k !== ' ') + .map(k => k.toUpperCase()) + .join(' '); + + card.innerHTML = ` + Stufe ${level.level} + ${keysDisplay}${level.newKeys.includes(' ') ? ' + Leertaste' : ''} + ${statusEmoji} + `; + + // Only current and completed levels are clickable + if (status !== 'locked') { + card.addEventListener('click', () => onLevelClick(level.level)); + } else { + card.disabled = true; + } + + container.appendChild(card); + }); + } + + export async function initForestScreen( + db: IDBDatabase, + onStartLesson: (level: number) => void + ): Promise { + const mapContainer = document.getElementById('level-map')!; + const continueBtn = document.getElementById('continue-btn') as HTMLButtonElement; + const progress = await getProgress(db); + if (!progress) return; + + renderLevelMap(mapContainer, progress, onStartLesson); + + // Continue button starts the current level + continueBtn.onclick = () => onStartLesson(progress.currentLevel); + } + ``` + + 3. Update src/app.ts to wire the complete flow: + - Import `initForestScreen` from `./forest/scene` + - Import `initLessonScreen` from `./ui/screens` + - In `initApp()`: after checking progress, if user exists, call `initForestScreen(db, startLesson)` before showing forest screen + - `startLesson(level)` function: show lesson screen, call initLessonScreen with onComplete callback + - `onComplete` callback: update progress (add to completedLevels, increment currentLevel if applicable, increment totalSessions, add today's date), save to DB, re-init forest screen, show forest screen + - After welcome screen's "Los geht's" navigates to forest: also call initForestScreen + + The full flow is: + - First visit: Welcome -> select companion/layout -> "Los geht's" -> Forest (level 1 = current) + - Click "Weiter ueben" or level 1 card -> Lesson screen (type letters) + - Complete lesson -> Forest (level 1 = completed, level 2 = current) + - Reload -> Forest (skips welcome, shows current state) + + 4. Add CSS for forest and level map to src/styles/main.css: + ```css + .forest { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + min-height: 100vh; + background: var(--bg-forest); + } + + .forest__title { + font-family: 'Quicksand', sans-serif; + font-size: 2rem; + color: var(--text-dark); + margin-bottom: 1.5rem; + } + + .forest__level-map { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + max-width: 500px; + width: 100%; + margin-bottom: 2rem; + } + + .level-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + padding: 1rem; + border-radius: 12px; + border: 2px solid transparent; + font-family: 'Nunito', sans-serif; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease; + background: white; + } + + .level-card:hover:not(:disabled) { + transform: scale(1.05); + } + + .level-card--current { + border-color: var(--accent-gold); + background: #FFF8E0; + box-shadow: 0 2px 8px rgba(232, 184, 75, 0.3); + } + + .level-card--completed { + border-color: var(--accent-green); + background: #E8F5E4; + } + + .level-card--locked { + opacity: 0.5; + cursor: not-allowed; + background: #f0f0f0; + } + + .level-card__number { + font-weight: 700; + font-size: 0.9rem; + color: var(--text-dark); + } + + .level-card__keys { + font-family: 'Quicksand', sans-serif; + font-weight: 600; + font-size: 1.1rem; + color: var(--accent-purple); + } + + .level-card__status { + font-size: 1.2rem; + } + + .forest__continue-btn { + padding: 1rem 3rem; + border-radius: 12px; + background: var(--accent-green); + color: white; + border: none; + font-family: 'Quicksand', sans-serif; + font-weight: 700; + font-size: 1.3rem; + cursor: pointer; + transition: transform 0.2s ease; + } + + .forest__continue-btn:hover { + transform: scale(1.05); + } + ``` +
+ + cd /home/dev/workspace/zauberwald && npx tsc --noEmit && npx vitest run + + + - src/forest/scene.ts exports `initForestScreen`, `renderLevelMap`, `getLevelStatus` + - src/forest/scene.ts contains `getLevelStatus` returning 'completed', 'current', or 'locked' + - src/forest/scene.ts creates 6 level cards with class `level-card` + - src/forest/scene.ts contains `level-card--${status}` for dynamic CSS class + - index.html contains `id="level-map"` and `id="continue-btn"` + - src/app.ts calls `initForestScreen` and `initLessonScreen` in the correct flow + - src/app.ts has lesson completion handler that calls `saveProgress` with updated `completedLevels` and `currentLevel` + - src/styles/main.css contains `.level-card--current`, `.level-card--completed`, `.level-card--locked` + - src/styles/main.css contains `.forest__continue-btn` + - `npx tsc --noEmit` exits 0 + - `npx vitest run` exits 0 + + Forest screen shows 6-level map with correct statuses, continue button starts current level, level cards are clickable for completed/current levels, complete app flow works end-to-end +
+ + + Task 2: Verify complete Phase 1 app flow + no files modified (verification only) + + Human verifies the complete Phase 1 app flow in the browser. No code changes in this task. + + Open the app at http://vps-ip:5173 and verify these 7 steps: + + 1. Welcome screen: 4 companion cards with emojis and DE/CH toggle visible. Select a companion, click "Los geht's". + 2. Forest overview: Level map shows Stufe 1 as current (gold border), Stufen 2-6 as locked (grayed). "Weiter ueben" button visible. + 3. Click "Weiter ueben": Lesson screen appears with a large letter (F, J, or space symbol) and QWERTZ keyboard below. Target key pulses on keyboard. + 4. Type the shown letter: Letter dissolves, next letter appears. Type all 8-12 letters correctly. + 5. After completion: Returns to forest. Stufe 1 now shows checkmark, Stufe 2 is current (gold). + 6. Reload the page: Skips welcome, goes directly to forest with preserved state. + 7. Wrong key test: During a lesson, press a wrong key — nothing happens to the letter, correct key continues pulsing. + + + cd /home/dev/workspace/zauberwald && npx tsc --noEmit && npx vitest run && echo "ALL AUTOMATED CHECKS PASS" + + + - All 7 manual verification steps pass in browser + - `npx tsc --noEmit` exits 0 + - `npx vitest run` exits 0 + + Human confirmed all 7 verification steps pass: welcome flow, forest map, lesson typing, level progression, persistence across reload, correct error handling + + +
+ + +- Complete flow: Welcome -> Forest -> Lesson -> Forest works +- Level map shows correct statuses after completing a level +- Typing exercise handles correct/wrong input per spec +- Progress persists across page reloads +- Keyboard shows finger colors for learned keys, gray for unlearned +- All tests pass: `npx vitest run` +- TypeScript compiles: `npx tsc --noEmit` + + + +- Forest overview shows 6 levels with locked/current/completed states +- "Weiter ueben" button starts the current level's typing exercise +- After completing a lesson, level status updates correctly +- Full app loop is playable from fresh start through at least level 1 completion +- Progress survives browser reload + + + +After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-05-SUMMARY.md` +