408 lines
15 KiB
Markdown
408 lines
15 KiB
Markdown
---
|
|
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-"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</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/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)
|
|
|
|
<interfaces>
|
|
From src/types.ts:
|
|
```typescript
|
|
export type KeyboardLayout = 'de' | 'ch';
|
|
|
|
export interface Level {
|
|
level: number;
|
|
newKeys: string[];
|
|
allKeys: string[];
|
|
fingerMap: Record<string, string>;
|
|
}
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Level definitions with key sets and finger mappings</name>
|
|
<files>
|
|
src/game/levels.ts, src/game/levels.test.ts
|
|
</files>
|
|
<read_first>
|
|
src/types.ts
|
|
SPEC.md (section 4.1 for progression table, section 8.3 for finger assignments)
|
|
</read_first>
|
|
<behavior>
|
|
- 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
|
|
</behavior>
|
|
<action>
|
|
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<string, string> = {
|
|
// 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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/dev/workspace/zauberwald && npx vitest run src/game/levels.test.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>Levels 1-6 defined with correct key sets per spec progression table, finger mapping matches spec section 8.3, all tests pass</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: QWERTZ keyboard renderer with finger colors and animations</name>
|
|
<files>
|
|
src/game/keyboard.ts, src/styles/main.css
|
|
</files>
|
|
<read_first>
|
|
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)
|
|
</read_first>
|
|
<action>
|
|
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<string, string> = {
|
|
'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<string, HTMLElement> = 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;
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/dev/workspace/zauberwald && npx tsc --noEmit && echo "PASS"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-03-SUMMARY.md`
|
|
</output>
|