chahinebrini 376f3454d6 feat(games,lyra): GameOverScreen migration + Lyra markdown-strip
GAMES (Nuxt → RN migration):
- New components/games/GameOverScreen.tsx — slide-in + fade overlay
  Props: score, bestScore, gameName, onRetry, onExit, isNewBest
- New lib/gameScores.ts — AsyncStorage helpers
  rebreak_best_snake (higher=better), _tetris (higher=better),
  _memory (lower=better, inverted isNewBest)
- UrgeGames.tsx wired: snake-collision/tetris-topout/memory-finish trigger
  GameOverScreen with retry/exit + best-score persist
- TicTacToe NICHT — round-aggregation game hat eigenen Fertig-Flow
- 7 i18n keys (gameOver.* DE+EN, 5 motivational texts statisch aus pool)

LYRA (markdown-bug fix):
- User-Report: Lyra antwortet mit ** in mobile-app, verwirrt user
- Beide system-prompts (COACH_SYSTEM_PROMPT für SOS, COACH_CASUAL_SYSTEM_PROMPT
  für Coach) bekommen "ANTWORTFORMAT - KRITISCH"-section:
  NIE Markdown (kein **bold**, _italic_, #-Headings, -Bullets) — Klartext only
- Reason: Mobile-App-bubbles rendern markdown nicht → User sieht raw `**text**`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:17:38 +02:00

257 lines
7.1 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Animated, Pressable, Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
import * as Haptics from 'expo-haptics';
import { useColors } from '../../lib/theme';
export type GameOverScreenProps = {
score: number;
bestScore: number;
gameName: string;
onRetry: () => void;
onExit: () => void;
isNewBest?: boolean;
};
const MOTIVATIONAL_KEYS = [
'gameOver.motivational_0',
'gameOver.motivational_1',
'gameOver.motivational_2',
'gameOver.motivational_3',
'gameOver.motivational_4',
];
export function GameOverScreen({
score,
bestScore,
gameName,
onRetry,
onExit,
isNewBest = false,
}: GameOverScreenProps) {
const { t } = useTranslation();
const colors = useColors();
const slideAnim = useRef(new Animated.Value(40)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
Animated.parallel([
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, tension: 60, friction: 10 }),
Animated.timing(fadeAnim, { toValue: 1, duration: 220, useNativeDriver: true }),
]).start();
}, []);
const motivationalKey = MOTIVATIONAL_KEYS[score % MOTIVATIONAL_KEYS.length]!;
const fmt = (n: number) => String(n).padStart(5, '0');
return (
<Animated.View
style={{
position: 'absolute',
inset: 0,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 24,
opacity: fadeAnim,
}}
>
{/* Backdrop */}
<Pressable
onPress={onExit}
style={{
position: 'absolute',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.55)',
}}
/>
{/* Card */}
<Animated.View
style={{
transform: [{ translateY: slideAnim }],
width: '100%',
maxWidth: 340,
backgroundColor: colors.surface,
borderRadius: 20,
paddingHorizontal: 20,
paddingTop: 24,
paddingBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.18,
shadowRadius: 20,
elevation: 12,
gap: 16,
}}
>
{/* Title row */}
<View style={{ alignItems: 'center', gap: 4 }}>
<Text
style={{
fontFamily: 'Nunito_800ExtraBold',
fontSize: 22,
color: colors.text,
textAlign: 'center',
}}
>
{t('gameOver.title')}
</Text>
<Text
style={{
fontFamily: 'Nunito_400Regular',
fontSize: 13,
color: colors.textMuted,
textAlign: 'center',
}}
>
{gameName}
</Text>
</View>
{/* Score row */}
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
gap: 12,
}}
>
{/* Score */}
<View
style={{
flex: 1,
backgroundColor: colors.surfaceElevated,
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: 8,
alignItems: 'center',
gap: 2,
}}
>
<Text
style={{
fontFamily: 'Courier New' as any,
fontSize: 22,
color: '#00e680',
letterSpacing: 2,
fontVariant: ['tabular-nums'],
}}
>
{fmt(score)}
</Text>
<Text
style={{
fontSize: 10,
color: colors.textMuted,
textTransform: 'uppercase',
letterSpacing: 1,
fontFamily: 'Nunito_600SemiBold',
}}
>
{t('gameOver.score')}
</Text>
</View>
{/* Best */}
<View
style={{
flex: 1,
backgroundColor: isNewBest ? '#fef3c7' : colors.surfaceElevated,
borderRadius: 14,
borderWidth: isNewBest ? 1.5 : 0,
borderColor: isNewBest ? '#f59e0b' : 'transparent',
paddingVertical: 12,
paddingHorizontal: 8,
alignItems: 'center',
gap: 2,
}}
>
<Text
style={{
fontFamily: 'Courier New' as any,
fontSize: 22,
color: isNewBest ? '#d97706' : colors.textMuted,
letterSpacing: 2,
fontVariant: ['tabular-nums'],
}}
>
{fmt(Math.max(score, bestScore))}
</Text>
<Text
style={{
fontSize: 10,
color: isNewBest ? '#d97706' : colors.textMuted,
textTransform: 'uppercase',
letterSpacing: 1,
fontFamily: 'Nunito_600SemiBold',
}}
>
{isNewBest ? t('gameOver.newBest') : t('gameOver.best')}
</Text>
</View>
</View>
{/* Motivational text */}
<Text
style={{
fontSize: 13,
color: colors.textMuted,
textAlign: 'center',
lineHeight: 19,
fontFamily: 'Nunito_400Regular',
paddingHorizontal: 4,
}}
>
{t(motivationalKey)}
</Text>
{/* Buttons */}
<View style={{ flexDirection: 'row', gap: 10 }}>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
onRetry();
}}
style={({ pressed }) => ({
flex: 1,
paddingVertical: 13,
paddingHorizontal: 16,
borderRadius: 12,
backgroundColor: '#007AFF',
alignItems: 'center',
opacity: pressed ? 0.75 : 1,
})}
>
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#ffffff' }}>
{t('gameOver.retry')}
</Text>
</Pressable>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
onExit();
}}
style={({ pressed }) => ({
flex: 1,
paddingVertical: 13,
paddingHorizontal: 16,
borderRadius: 12,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
opacity: pressed ? 0.75 : 1,
})}
>
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.textMuted }}>
{t('gameOver.exit')}
</Text>
</Pressable>
</View>
</Animated.View>
</Animated.View>
);
}