--- 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`