fix(games): Tetris controls centered + Snake icon visibility + digital score-dashboard
User-Wünsche:
1. Tetris bedien-buttons mittig zum Spielfeld (war off-center)
2. Snake geklickte button-icons NICHT weiß (sonst light-theme unsichtbar)
3. Beide games: digital score-counter über playfield
Tetris:
- Controls in alignItems:'center'-wrapper mit width:boardWidth child +
justifyContent:'space-between' → Move-Pad+Action-Pad bündig zum Feld
unabhängig von screen-width
- Old Score/Level/Lines header entfernt → DigitalScore übernimmt
Snake:
- DPadBtn: ALWAYS color={tint} (#007aff iOS-blue) für Ionicons
- Active-state via borderColor + scale(1.04), NICHT mehr durch white-icon
- Semi-transparent blue bg (rgba) sichtbar in beiden themes
- Android-Branches + elevation entfernt (überall einheitlich)
DigitalScore (neu):
- 7-segment-feel via Courier New monospace + letterSpacing 2 + tabular-nums
- padStart(5,'0') Score+Best, padStart(2,'0') Level/Length
- Dunkles Panel (#0d1117) + border #1f2937, intentional contrast
- width:boardWidth, alignSelf:center
- Snake: SCORE+BEST | Tetris: SCORE+BEST+LVL
TS clean. Frontend-only, Metro reload reicht.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
417191c90a
commit
c9029b8fb5
@ -9,6 +9,7 @@ import * as Haptics from 'expo-haptics';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { memorySvg, snakeSvg, tetrisSvg, tictactoeSvg } from './gameSvgs';
|
||||
import { useColors } from '../../lib/theme';
|
||||
|
||||
// Haptic helper — fire-and-forget, swallow errors on platforms without taptic engine
|
||||
function tapHaptic() {
|
||||
@ -282,23 +283,16 @@ export function SnakeGame({
|
||||
return (
|
||||
<View style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: Math.max(insets.bottom, 16) }}>
|
||||
{/* Header */}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<Text style={{ fontSize: 11, color: '#6b7280', flex: 1, marginRight: 8 }} numberOfLines={2}>{lyraMessage}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 14 }}>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 9, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: 0.5 }}>Score</Text>
|
||||
<Text style={{ fontSize: 18, fontFamily: 'Nunito_800ExtraBold', color: '#16a34a' }}>{score}</Text>
|
||||
</View>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 9, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: 0.5 }}>Best</Text>
|
||||
<Text style={{ fontSize: 18, fontFamily: 'Nunito_800ExtraBold', color: '#111827' }}>{highScore}</Text>
|
||||
</View>
|
||||
<Pressable onPress={onAbandon} hitSlop={10} style={{ width: 28, height: 28, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text style={{ fontSize: 18, color: '#6b7280' }}>✕</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable onPress={onAbandon} hitSlop={10} style={{ width: 28, height: 28, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text style={{ fontSize: 18, color: '#6b7280' }}>✕</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Digital score dashboard */}
|
||||
<DigitalScore score={score} best={highScore} boardWidth={boardW} />
|
||||
|
||||
{/* Board */}
|
||||
<View style={{ alignItems: 'center' }} {...panResponder.panHandlers}>
|
||||
<View style={{ width: boardW, height: boardH, borderRadius: 12, overflow: 'hidden', borderWidth: 1, borderColor: 'rgba(0,230,128,0.2)', backgroundColor: '#0d1117' }}>
|
||||
@ -352,8 +346,6 @@ export function SnakeGame({
|
||||
);
|
||||
}
|
||||
|
||||
// Platform-native D-Pad button: iOS uses system-blue tinted circle (SF-symbol look),
|
||||
// Android uses Material elevated surface with ripple.
|
||||
function DPadBtn({ dir, active, onPress }: { dir: Dir; active: boolean; onPress: () => void }) {
|
||||
const icons: Record<Dir, 'chevron-up' | 'chevron-down' | 'chevron-back' | 'chevron-forward'> = {
|
||||
up: 'chevron-up', down: 'chevron-down', left: 'chevron-back', right: 'chevron-forward',
|
||||
@ -366,29 +358,24 @@ function DPadBtn({ dir, active, onPress }: { dir: Dir; active: boolean; onPress:
|
||||
hitSlop={12}
|
||||
android_ripple={{ color: 'rgba(0,122,255,0.22)', borderless: true, radius: 32 }}
|
||||
style={({ pressed }) => {
|
||||
const bgIdle = isIOS ? 'rgba(0,122,255,0.10)' : '#ffffff';
|
||||
const bgPressed = isIOS ? 'rgba(0,122,255,0.22)' : '#f5f5f5';
|
||||
const bgActive = tint;
|
||||
const bg = active ? bgActive : (pressed && isIOS ? bgPressed : bgIdle);
|
||||
const bgIdle = 'rgba(0,122,255,0.10)';
|
||||
const bgPressed = 'rgba(0,122,255,0.22)';
|
||||
const bgActive = 'rgba(0,122,255,0.22)';
|
||||
const bg = active ? bgActive : pressed ? bgPressed : bgIdle;
|
||||
return {
|
||||
width: 60, height: 60, borderRadius: 30,
|
||||
backgroundColor: bg,
|
||||
borderWidth: 1.5,
|
||||
borderColor: active ? tint : 'rgba(0,122,255,0.30)',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
...(isIOS ? {} : {
|
||||
elevation: active ? 4 : 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 2,
|
||||
}),
|
||||
transform: [{ scale: pressed && isIOS ? 0.96 : 1 }],
|
||||
transform: [{ scale: pressed ? 0.96 : active ? 1.04 : 1 }],
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={icons[dir]}
|
||||
size={28}
|
||||
color={active ? '#ffffff' : tint}
|
||||
color={tint}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
@ -966,20 +953,19 @@ export function TetrisGame({
|
||||
|
||||
const speedColors = ['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444'];
|
||||
|
||||
const boardWidth = TETRIS_COLS * CELL;
|
||||
|
||||
return (
|
||||
<View style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: Math.max(insets.bottom, 16) }}>
|
||||
{/* Header */}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<Text style={{ fontSize: 11, color: '#6b7280', flex: 1, marginRight: 8 }} numberOfLines={2}>{lyraMessage}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
||||
<Stat label="Score" value={score} color="#111827" />
|
||||
{highScore > 0 && <Stat label="Best" value={highScore} color="#f59e0b" />}
|
||||
<Stat label="Lvl" value={level} color="#3b82f6" />
|
||||
<Stat label="Lines" value={lines} color="#111827" />
|
||||
<Pressable onPress={onAbandon} hitSlop={10}><Text style={{ fontSize: 18, color: '#6b7280' }}>✕</Text></Pressable>
|
||||
</View>
|
||||
<Pressable onPress={onAbandon} hitSlop={10}><Text style={{ fontSize: 18, color: '#6b7280' }}>✕</Text></Pressable>
|
||||
</View>
|
||||
|
||||
{/* Digital score dashboard */}
|
||||
<DigitalScore score={score} best={highScore} extra={level} extraLabel="LVL" boardWidth={boardWidth} />
|
||||
|
||||
{/* Board */}
|
||||
<View style={{ alignItems: 'center', marginVertical: 4 }}>
|
||||
<View style={{ width: TETRIS_COLS * CELL, height: TETRIS_ROWS * CELL, borderRadius: 12, overflow: 'hidden', backgroundColor: '#0d1117', borderWidth: 1, borderColor: '#1f2937' }}>
|
||||
@ -1037,17 +1023,24 @@ export function TetrisGame({
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Controls — Move Pad (links) + Action Pad (rechts) */}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 18 }}>
|
||||
{/* Move Pad */}
|
||||
<View style={{ flexDirection: 'row', gap: 14 }}>
|
||||
<DPadBtn dir="left" active={false} onPress={moveLeft} />
|
||||
<DPadBtn dir="right" active={false} onPress={moveRight} />
|
||||
</View>
|
||||
{/* Action Pad */}
|
||||
<View style={{ flexDirection: 'row', gap: 14 }}>
|
||||
<TetrisActionBtn icon="sync" label="Drehen" accent="#7c3aed" onPress={rotatePiece} />
|
||||
<TetrisActionBtn icon="arrow-down" label="Drop" accent="#0ea5e9" onPress={softDrop} />
|
||||
{/* Controls — aligned to board width, centered on screen */}
|
||||
<View style={{ alignItems: 'center', marginTop: 18 }}>
|
||||
<View style={{
|
||||
width: boardWidth,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
{/* Move Pad */}
|
||||
<View style={{ flexDirection: 'row', gap: 14 }}>
|
||||
<DPadBtn dir="left" active={false} onPress={moveLeft} />
|
||||
<DPadBtn dir="right" active={false} onPress={moveRight} />
|
||||
</View>
|
||||
{/* Action Pad */}
|
||||
<View style={{ flexDirection: 'row', gap: 14 }}>
|
||||
<TetrisActionBtn icon="sync" label="Drehen" accent="#7c3aed" onPress={rotatePiece} />
|
||||
<TetrisActionBtn icon="arrow-down" label="Drop" accent="#0ea5e9" onPress={softDrop} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -1062,10 +1055,75 @@ export function TetrisGame({
|
||||
}
|
||||
|
||||
function Stat({ label, value, color }: { label: string; value: number; color: string }) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 9, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: 0.5 }}>{label}</Text>
|
||||
<Text style={{ fontSize: 9, color: colors.textMuted, textTransform: 'uppercase', letterSpacing: 0.5 }}>{label}</Text>
|
||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color }}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function DigitalScore({
|
||||
score,
|
||||
best,
|
||||
extra,
|
||||
extraLabel,
|
||||
boardWidth,
|
||||
}: {
|
||||
score: number;
|
||||
best: number;
|
||||
extra?: number;
|
||||
extraLabel?: string;
|
||||
boardWidth: number;
|
||||
}) {
|
||||
const fmt = (n: number, digits = 5) => String(n).padStart(digits, '0');
|
||||
return (
|
||||
<View style={{
|
||||
width: boardWidth,
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#0d1117',
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: '#1f2937',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 7,
|
||||
marginBottom: 6,
|
||||
}}>
|
||||
<ScoreCell label="SCORE" value={fmt(score)} bright />
|
||||
<View style={{ width: 1, height: 28, backgroundColor: '#1f2937' }} />
|
||||
<ScoreCell label="BEST" value={fmt(best)} />
|
||||
{extra !== undefined && extraLabel !== undefined && (
|
||||
<>
|
||||
<View style={{ width: 1, height: 28, backgroundColor: '#1f2937' }} />
|
||||
<ScoreCell label={extraLabel} value={fmt(extra, 2)} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ScoreCell({ label, value, bright }: { label: string; value: string; bright?: boolean }) {
|
||||
return (
|
||||
<View style={{ alignItems: 'center', flex: 1 }}>
|
||||
<Text style={{
|
||||
fontSize: 9,
|
||||
color: '#4b5563',
|
||||
letterSpacing: 1.5,
|
||||
fontFamily: 'Nunito_600SemiBold',
|
||||
textTransform: 'uppercase',
|
||||
}}>{label}</Text>
|
||||
<Text style={{
|
||||
fontSize: 18,
|
||||
fontFamily: 'Courier New' as any,
|
||||
fontVariant: ['tabular-nums'],
|
||||
color: bright ? '#00e680' : '#6b7280',
|
||||
letterSpacing: 2,
|
||||
lineHeight: 22,
|
||||
}}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user